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 9a38dfc65..b037250ef 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -52,7 +52,7 @@ This gives Adobe permission to redistribute your contributions as part of the pr 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 @@ -179,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 index e75a0f228..a9570ebee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.3', '7.4'] + php-versions: ['8.2', '8.3', '8.4'] steps: - uses: actions/checkout@v2 @@ -25,7 +25,7 @@ jobs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} @@ -39,14 +39,14 @@ jobs: - 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 +# - 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 @@ -54,7 +54,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.3', '7.4'] + php-versions: ['8.2', '8.3', '8.4'] steps: - uses: actions/checkout@v2 @@ -66,7 +66,7 @@ jobs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} @@ -86,7 +86,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.3', '7.4'] + php-versions: ['8.2', '8.3', '8.4'] steps: - uses: actions/checkout@v2 @@ -98,7 +98,7 @@ jobs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} @@ -118,7 +118,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.3', '7.4'] + php-versions: ['8.2', '8.3', '8.4'] services: chrome: @@ -136,7 +136,7 @@ jobs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} diff --git a/.gitignore b/.gitignore index 3166f9529..419218ba4 100755 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ 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/CHANGELOG.md b/CHANGELOG.md index 328824514..14e864cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,414 @@ Magento Functional Testing Framework Changelog ================================================ + +5.0.3 +--------- +### Fixes +* Allowed additional actions to read from credentials file to fix page builder failures. +* Added support for chrome 131 + +5.0.2 +--------- +### Fixes +* Removed support for chrome 131 + +5.0.1 +--------- +### Enhancement +* Provided support for chrome 131 + +5.0.0 +--------- +### Enhancements +* Provided support for PHP 8.4 +* Dropped the support for PHP 8.1 +* Removed unwanted dependent packages +* Removed the dependency of codeception/module-sequence and implemented internal adjustments to address PHP 8.4 deprecations. + +4.8.3 +--------- +### Enhancements +* Bumped aws/aws-sdk-php package to 3.323.4 +* Bumped composer/composer to 2.8.1 +* Bumped laminas/laminas-diactoros to 3.4.1 +* Bumped nikic/php-parser to 5.3.1 +* Bumped squizlabs/php_codesniffer to 3.10.3 +* Remove any unused files remaining after upgrading Codeception. + +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 --------- @@ -7,14 +416,13 @@ Magento Functional Testing Framework Changelog * [#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. + * 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 @@ -33,7 +441,7 @@ Magento Functional Testing Framework Changelog * Maintainability * Added support for composer 2. - + 3.3.0 --------- @@ -41,11 +449,11 @@ Magento Functional Testing Framework Changelog * 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 - + * [#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. @@ -71,8 +479,8 @@ Magento Functional Testing Framework Changelog * 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 + +* Maintainability * Updated annotation static-check to check all required annotations. ### Fixes @@ -87,7 +495,7 @@ Magento Functional Testing Framework Changelog * Removed `travis.yml` and replaced with `.github/workflows/main.yml` ### Fixes -Fixed issue with XPath locators for waits in MagentoPwaWebDriver. +Fixed issue with XPath locators for waits in MagentoPwaWebDriver. 3.1.0 --------- @@ -101,13 +509,13 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. * 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 + +* 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 + +* 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. @@ -123,7 +531,7 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. * [#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 + * [#745](https://github.com/magento/magento2-functional-testing-framework/pull/745) -- Docs: Remove invalid sample test name 3.0.0 --------- @@ -134,22 +542,22 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. * 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 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) + * 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`. + * 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) @@ -158,19 +566,19 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. * `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) + * 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). + * 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. - + * 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. @@ -258,7 +666,7 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. * 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 @@ -310,12 +718,12 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. ----- * 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. @@ -382,7 +790,7 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. * 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. @@ -484,7 +892,7 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. ### 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. @@ -573,15 +981,15 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. 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). @@ -632,7 +1040,7 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. 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. @@ -709,7 +1117,7 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. ----- ### 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. @@ -745,7 +1153,7 @@ Fixed issue with XPath locators for waits in MagentoPwaWebDriver. * 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/README.md b/README.md index 5a4bbab11..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 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 15a28c947..f9cd0bcd9 100755 --- a/composer.json +++ b/composer.json @@ -2,57 +2,57 @@ "name": "magento/magento2-functional-testing-framework", "description": "Magento2 Functional Testing Framework", "type": "library", - "version": "3.5.1", + "version": "5.0.3", "license": "AGPL-3.0", "keywords": ["magento", "automation", "functional", "testing"], "config": { "sort-packages": true }, "require": { - "php": "^7.3", + "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.4", - "aws/aws-sdk-php": "^3.132", - "codeception/codeception": "^4.1", - "codeception/module-asserts": "^1.1", - "codeception/module-sequence": "^1.0", - "codeception/module-webdriver": "^1.0", - "composer/composer": "^1.9||^2.0", - "csharpru/vault-php": "^4.2.1", - "csharpru/vault-php-guzzle6-transport": "^2.0", - "hoa/console": "~3.0", - "monolog/monolog": "^2.2", + "guzzlehttp/guzzle": "^7.3.0", + "laminas/laminas-diactoros": "^3.0", + "monolog/monolog": "^2.3||^3.0", "mustache/mustache": "~2.5", - "php-webdriver/webdriver": "^1.9.0", - "spomky-labs/otphp": "^10.0", - "symfony/console": "^4.4", - "symfony/finder": "^5.0", - "symfony/http-foundation": "^5.0", - "symfony/mime": "^5.0", - "symfony/process": "^4.4", - "vlucas/phpdotenv": "^2.4", - "weew/helpers-array": "^1.3", - "nikic/php-parser": "^4.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": { - "brainmaestro/composer-git-hooks": "^2.3.1", - "codacy/coverage": "^1.4", - "codeception/aspect-mock": "^3.0", + "brainmaestro/composer-git-hooks": "^3.0", "php-coveralls/php-coveralls": "^1.0||^2.2", "phpmd/phpmd": "^2.8.0", - "phpunit/phpunit": "^9.0", - "sebastian/phpcpd": "~6.0.0", - "squizlabs/php_codesniffer": "~3.5.4" + "phpunit/phpunit": "^10.0", + "squizlabs/php_codesniffer": "~3.10.1" + }, + "suggest": { + "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": { diff --git a/composer.lock b/composer.lock index cfc6e4f9e..fb9e910e8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,38 +4,39 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6e848e638ba77ff322a62b24136fb06a", + "content-hash": "7556e816d320192bd4d43e1710d665d8", "packages": [ { "name": "allure-framework/allure-codeception", - "version": "1.5.2", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "a6156aef942a4e4de0add34a73d066a9458cefc6" + "reference": "854320894b5e65952eb0cafd1555e9efb4543350" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/a6156aef942a4e4de0add34a73d066a9458cefc6", - "reference": "a6156aef942a4e4de0add34a73d066a9458cefc6", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/854320894b5e65952eb0cafd1555e9efb4543350", + "reference": "854320894b5e65952eb0cafd1555e9efb4543350", "shasum": "" }, "require": { - "allure-framework/allure-php-api": "^1.3", - "codeception/codeception": "^2.5 | ^3 | ^4", + "allure-framework/allure-php-commons": "^2.3.1", + "codeception/codeception": "^5.0.3", "ext-json": "*", - "php": ">=7.1.3", - "symfony/filesystem": "^2.7 | ^3 | ^4 | ^5", - "symfony/finder": "^2.7 | ^3 | ^4 | ^5" + "php": "^8" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^7.2 | ^8 | ^9" + "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/", @@ -47,10 +48,15 @@ "name": "Ivan Krutov", "email": "vania-pooh@aerokube.com", "role": "Developer" + }, + { + "name": "Edward Surov", + "email": "zoohie@gmail.com", + "role": "Developer" } ], "description": "Allure Codeception integration", - "homepage": "http://allure.qatools.ru/", + "homepage": "https://allurereport.org/", "keywords": [ "allure", "attachments", @@ -65,38 +71,44 @@ "issues": "https://github.com/allure-framework/allure-codeception/issues", "source": "https://github.com/allure-framework/allure-codeception" }, - "time": "2021-06-04T13:24:36+00:00" + "time": "2024-05-28T09:54:01+00:00" }, { - "name": "allure-framework/allure-php-api", - "version": "1.3.1", + "name": "allure-framework/allure-php-commons", + "version": "v2.3.1", "source": { "type": "git", - "url": "https://github.com/allure-framework/allure-php-commons.git", - "reference": "f64b69afeff472c564a4e2379efb2b69c430ec5a" + "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/f64b69afeff472c564a4e2379efb2b69c430ec5a", - "reference": "f64b69afeff472c564a4e2379efb2b69c430ec5a", + "url": "https://api.github.com/repos/allure-framework/allure-php-commons2/zipball/5d7ed5ab510339652163ca1473eb499d4b7ec488", + "reference": "5d7ed5ab510339652163ca1473eb499d4b7ec488", "shasum": "" }, "require": { - "jms/serializer": "^1 | ^2 | ^3", - "php": ">=7.1.3", - "ramsey/uuid": "^3 | ^4", - "symfony/mime": "^4.3 | ^5" + "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": "^7 | ^8 | ^9" + "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/", @@ -105,65 +117,129 @@ ], "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" ], "support": { - "email": "allure@yandex-team.ru", - "issues": "https://github.com/allure-framework/allure-php-commons/issues", - "source": "https://github.com/allure-framework/allure-php-api" + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" }, - "time": "2021-03-26T14:32:27+00:00" + "time": "2024-10-18T22:15:13+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.180.1", + "version": "3.342.28", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "7801112fd8be227954a6ecfbfd85b01ee4a7cae4" + "reference": "16cec140483869b3d244a5995b55d5365465dc58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7801112fd8be227954a6ecfbfd85b01ee4a7cae4", - "reference": "7801112fd8be227954a6ecfbfd85b01ee4a7cae4", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/16cec140483869b3d244a5995b55d5365465dc58", + "reference": "16cec140483869b3d244a5995b55d5365465dc58", "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.4.0", - "guzzlehttp/psr7": "^1.7.0", - "mtdowling/jmespath.php": "^2.6", - "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", - "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^4.8.35|^5.4.3", - "psr/cache": "^1.0", - "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3" + "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", @@ -179,11 +255,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/", @@ -209,101 +288,39 @@ "sdk" ], "support": { - "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "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.180.1" - }, - "time": "2021-05-04T18:14:38+00:00" - }, - { - "name": "beberlei/assert", - "version": "v3.3.1", - "source": { - "type": "git", - "url": "https://github.com/beberlei/assert.git", - "reference": "5e721d7e937ca3ba2cdec1e1adf195f9e5188372" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/beberlei/assert/zipball/5e721d7e937ca3ba2cdec1e1adf195f9e5188372", - "reference": "5e721d7e937ca3ba2cdec1e1adf195f9e5188372", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-simplexml": "*", - "php": "^7.0 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "*", - "phpstan/phpstan": "*", - "phpunit/phpunit": ">=6.0.0", - "yoast/phpunit-polyfills": "^0.1.0" - }, - "suggest": { - "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" - }, - "type": "library", - "autoload": { - "psr-4": { - "Assert\\": "lib/Assert" - }, - "files": [ - "lib/Assert/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "authors": [ - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de", - "role": "Lead Developer" - }, - { - "name": "Richard Quadling", - "email": "rquadling@gmail.com", - "role": "Collaborator" - } - ], - "description": "Thin assertion library for input validation in business models.", - "keywords": [ - "assert", - "assertion", - "validation" - ], - "support": { - "issues": "https://github.com/beberlei/assert/issues", - "source": "https://github.com/beberlei/assert/tree/v3.3.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.342.28" }, - "time": "2021-04-18T20:11:03+00:00" + "time": "2025-04-16T18:13:32+00:00" }, { "name": "behat/gherkin", - "version": "v4.8.0", + "version": "v4.12.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "2391482cd003dfdc36b679b27e9f5326bd656acd" + "reference": "cc3a7e224b36373be382b53ef02ede0f1807bb58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/2391482cd003dfdc36b679b27e9f5326bd656acd", - "reference": "2391482cd003dfdc36b679b27e9f5326bd656acd", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/cc3a7e224b36373be382b53ef02ede0f1807bb58", + "reference": "cc3a7e224b36373be382b53ef02ede0f1807bb58", "shasum": "" }, "require": { - "php": "~7.2|~8.0" + "composer-runtime-api": "^2.2", + "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" }, "require-dev": { - "cucumber/cucumber": "dev-gherkin-16.0.0", - "phpunit/phpunit": "~8|~9", - "symfony/phpunit-bridge": "~3|~4|~5", - "symfony/yaml": "~3|~4|~5" + "cucumber/cucumber": "dev-gherkin-24.1.0", + "friendsofphp/php-cs-fixer": "^3.65", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpunit/phpunit": "^10.5", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "suggest": { "symfony/yaml": "If you want to parse features, represented in YAML files" @@ -311,7 +328,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "4.x-dev" } }, "autoload": { @@ -327,11 +344,11 @@ { "name": "Konstantin Kudryashov", "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" + "homepage": "https://everzet.com" } ], "description": "Gherkin DSL parser for PHP", - "homepage": "http://behat.org/", + "homepage": "https://behat.org/", "keywords": [ "BDD", "Behat", @@ -342,32 +359,31 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.8.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.12.0" }, - "time": "2021-02-04T12:44:21+00:00" + "time": "2025-02-26T14:28:23+00:00" }, { "name": "brick/math", - "version": "0.9.2", + "version": "0.12.3", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0" + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/dff976c2f3487d42c1db75a3b180e2b9f0e72ce0", - "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", "shasum": "" }, "require": { - "ext-json": "*", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", - "vimeo/psalm": "4.3.2" + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "6.8.8" }, "type": "library", "autoload": { @@ -387,83 +403,114 @@ "arithmetic", "bigdecimal", "bignum", + "bignumber", "brick", - "math" + "decimal", + "integer", + "math", + "mathematics", + "rational" ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.9.2" + "source": "https://github.com/brick/math/tree/0.12.3" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/brick/math", - "type": "tidelift" + "url": "https://github.com/BenMorel", + "type": "github" } ], - "time": "2021-01-20T22:51:39+00:00" + "time": "2025-02-28T13:11:00+00:00" }, { "name": "codeception/codeception", - "version": "4.1.21", + "version": "5.2.1", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "c25f20d842a7e3fa0a8e6abf0828f102c914d419" + "reference": "6e06224627dcd89e7d4753f44ba4df35034b6314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/c25f20d842a7e3fa0a8e6abf0828f102c914d419", - "reference": "c25f20d842a7e3fa0a8e6abf0828f102c914d419", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/6e06224627dcd89e7d4753f44ba4df35034b6314", + "reference": "6e06224627dcd89e7d4753f44ba4df35034b6314", "shasum": "" }, "require": { - "behat/gherkin": "^4.4.0", - "codeception/lib-asserts": "^1.0", - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.1.1 | ^9.0", - "codeception/stub": "^2.0 | ^3.0", + "behat/gherkin": "^4.6.2", + "codeception/lib-asserts": "^2.0", + "codeception/stub": "^4.1", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "guzzlehttp/psr7": "~1.4", - "php": ">=5.6.0 <9.0", - "symfony/console": ">=2.7 <6.0", - "symfony/css-selector": ">=2.7 <6.0", - "symfony/event-dispatcher": ">=2.7 <6.0", - "symfony/finder": ">=2.7 <6.0", - "symfony/yaml": ">=2.7 <6.0" + "php": "^8.1", + "phpunit/php-code-coverage": "^9.2 || ^10.0 || ^11.0 || ^12.0", + "phpunit/php-text-template": "^2.0 || ^3.0 || ^4.0 || ^5.0", + "phpunit/php-timer": "^5.0.3 || ^6.0 || ^7.0 || ^8.0", + "phpunit/phpunit": "^9.5.20 || ^10.0 || ^11.0 || ^12.0", + "psy/psysh": "^0.11.2 || ^0.12", + "sebastian/comparator": "^4.0.5 || ^5.0 || ^6.0 || ^7.0", + "sebastian/diff": "^4.0.3 || ^5.0 || ^6.0 || ^7.0", + "symfony/console": ">=5.4.24 <8.0", + "symfony/css-selector": ">=5.4.24 <8.0", + "symfony/event-dispatcher": ">=5.4.24 <8.0", + "symfony/finder": ">=5.4.24 <8.0", + "symfony/var-dumper": ">=5.4.24 <8.0", + "symfony/yaml": ">=5.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/module-asserts": "1.*@dev", - "codeception/module-cli": "1.*@dev", - "codeception/module-db": "1.*@dev", - "codeception/module-filesystem": "1.*@dev", - "codeception/module-phpbrowser": "1.*@dev", - "codeception/specify": "~0.3", + "codeception/lib-innerbrowser": "*@dev", + "codeception/lib-web": "*@dev", + "codeception/module-asserts": "*@dev", + "codeception/module-cli": "*@dev", + "codeception/module-db": "*@dev", + "codeception/module-filesystem": "*@dev", + "codeception/module-phpbrowser": "*@dev", "codeception/util-universalframework": "*@dev", - "monolog/monolog": "~1.8", - "squizlabs/php_codesniffer": "~2.0", - "symfony/process": ">=2.7 <6.0", - "vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0 | ^5.0" + "ext-simplexml": "*", + "jetbrains/phpstorm-attributes": "^1.0", + "symfony/dotenv": ">=5.4.24 <8.0", + "symfony/process": ">=5.4.24 <8.0", + "vlucas/phpdotenv": "^5.1" }, "suggest": { "codeception/specify": "BDD-style code blocks", "codeception/verify": "BDD-style assertions", - "hoa/console": "For interactive console functionality", + "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": [] + "branch-alias": { + "dev-main": "5.2.x-dev" + } }, "autoload": { + "files": [ + "functions.php" + ], "psr-4": { "Codeception\\": "src/Codeception", "Codeception\\Extension\\": "ext" - } + }, + "classmap": [ + "src/PHPUnit/TestCase.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -472,12 +519,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", @@ -487,7 +534,7 @@ ], "support": { "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/4.1.21" + "source": "https://github.com/Codeception/Codeception/tree/5.2.1" }, "funding": [ { @@ -495,26 +542,26 @@ "type": "open_collective" } ], - "time": "2021-05-28T17:43:39+00:00" + "time": "2025-02-20T14:52:49+00:00" }, { "name": "codeception/lib-asserts", - "version": "1.13.2", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6" + "reference": "06750a60af3ebc66faab4313981accec1be4eefc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/184231d5eab66bc69afd6b9429344d80c67a33b6", - "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/06750a60af3ebc66faab4313981accec1be4eefc", + "reference": "06750a60af3ebc66faab4313981accec1be4eefc", "shasum": "" }, "require": { - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0", + "codeception/phpunit-wrapper": "^7.7.1 | ^8.0.3 | ^9.0", "ext-dom": "*", - "php": ">=5.6.0 <9.0" + "php": "^7.4 | ^8.0" }, "type": "library", "autoload": { @@ -547,31 +594,36 @@ ], "support": { "issues": "https://github.com/Codeception/lib-asserts/issues", - "source": "https://github.com/Codeception/lib-asserts/tree/1.13.2" + "source": "https://github.com/Codeception/lib-asserts/tree/2.2.0" }, - "time": "2020-10-21T16:26:20+00:00" + "time": "2025-03-10T20:41:33+00:00" }, { - "name": "codeception/module-asserts", - "version": "1.3.1", + "name": "codeception/lib-web", + "version": "1.0.7", "source": { "type": "git", - "url": "https://github.com/Codeception/module-asserts.git", - "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de" + "url": "https://github.com/Codeception/lib-web.git", + "reference": "1444ccc9b1d6721f3ced8703c8f4a9041b80df93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/59374f2fef0cabb9e8ddb53277e85cdca74328de", - "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de", + "url": "https://api.github.com/repos/Codeception/lib-web/zipball/1444ccc9b1d6721f3ced8703c8f4a9041b80df93", + "reference": "1444ccc9b1d6721f3ced8703c8f4a9041b80df93", "shasum": "" }, "require": { - "codeception/codeception": "*@dev", - "codeception/lib-asserts": "^1.13.1", - "php": ">=5.6.0 <9.0" + "ext-mbstring": "*", + "guzzlehttp/psr7": "^2.0", + "php": "^8.1", + "phpunit/phpunit": "^9.5 | ^10.0 | ^11.0 | ^12", + "symfony/css-selector": ">=4.4.24 <8.0" }, "conflict": { - "codeception/codeception": "<4.0" + "codeception/codeception": "<5.0.0-alpha3" + }, + "require-dev": { + "php-webdriver/webdriver": "^1.12" }, "type": "library", "autoload": { @@ -584,47 +636,42 @@ "MIT" ], "authors": [ - { - "name": "Michael Bodnarchuk" - }, { "name": "Gintautas Miselis" - }, - { - "name": "Gustavo Nieves", - "homepage": "https://medium.com/@ganieves" } ], - "description": "Codeception module containing various assertions", + "description": "Library containing files used by module-webdriver and lib-innerbrowser or module-phpbrowser", "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/1.3.1" + "issues": "https://github.com/Codeception/lib-web/issues", + "source": "https://github.com/Codeception/lib-web/tree/1.0.7" }, - "time": "2020-10-21T16:48:15+00:00" + "time": "2025-02-09T12:05:55+00:00" }, { - "name": "codeception/module-sequence", - "version": "1.0.1", + "name": "codeception/module-asserts", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/Codeception/module-sequence.git", - "reference": "b75be26681ae90824cde8f8df785981f293667e1" + "url": "https://github.com/Codeception/module-asserts.git", + "reference": "1b6b150b30586c3614e7e5761b31834ed7968603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-sequence/zipball/b75be26681ae90824cde8f8df785981f293667e1", - "reference": "b75be26681ae90824cde8f8df785981f293667e1", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/1b6b150b30586c3614e7e5761b31834ed7968603", + "reference": "1b6b150b30586c3614e7e5761b31834ed7968603", "shasum": "" }, "require": { - "codeception/codeception": "^4.0", - "php": ">=5.6.0 <9.0" + "codeception/codeception": "*@dev", + "codeception/lib-asserts": "^2.0", + "php": "^8.0" + }, + "conflict": { + "codeception/codeception": "<5.0" }, "type": "library", "autoload": { @@ -639,37 +686,51 @@ "authors": [ { "name": "Michael Bodnarchuk" + }, + { + "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" } ], - "description": "Sequence module for Codeception", - "homepage": "http://codeception.com/", + "description": "Codeception module containing various assertions", + "homepage": "https://codeception.com/", "keywords": [ + "assertions", + "asserts", "codeception" ], "support": { - "issues": "https://github.com/Codeception/module-sequence/issues", - "source": "https://github.com/Codeception/module-sequence/tree/1.0.1" + "issues": "https://github.com/Codeception/module-asserts/issues", + "source": "https://github.com/Codeception/module-asserts/tree/3.0.0" }, - "time": "2020-10-31T18:36:26+00:00" + "time": "2022-02-16T19:48:08+00:00" }, { "name": "codeception/module-webdriver", - "version": "1.2.1", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "ebbe729c630415e8caf6b0087e457906f0c6c0c6" + "reference": "551d214ddd57a9539acf1123d7c56ec82b1004df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/ebbe729c630415e8caf6b0087e457906f0c6c0c6", - "reference": "ebbe729c630415e8caf6b0087e457906f0c6c0c6", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/551d214ddd57a9539acf1123d7c56ec82b1004df", + "reference": "551d214ddd57a9539acf1123d7c56ec82b1004df", "shasum": "" }, "require": { - "codeception/codeception": "^4.0", - "php": ">=5.6.0 <9.0", - "php-webdriver/webdriver": "^1.8.0" + "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 || ^12.0" }, "suggest": { "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" @@ -696,7 +757,7 @@ } ], "description": "WebDriver module for Codeception", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "acceptance-testing", "browser-testing", @@ -704,117 +765,153 @@ ], "support": { "issues": "https://github.com/Codeception/module-webdriver/issues", - "source": "https://github.com/Codeception/module-webdriver/tree/1.2.1" + "source": "https://github.com/Codeception/module-webdriver/tree/4.0.3" }, - "time": "2021-04-23T17:30:57+00:00" + "time": "2025-02-14T07:10:05+00:00" }, { - "name": "codeception/phpunit-wrapper", - "version": "9.0.6", + "name": "codeception/stub", + "version": "4.1.4", "source": { "type": "git", - "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "b0c06abb3181eedca690170f7ed0fd26a70bfacc" + "url": "https://github.com/Codeception/Stub.git", + "reference": "6ce453073a0c220b254dd7f4383645615e4071c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/b0c06abb3181eedca690170f7ed0fd26a70bfacc", - "reference": "b0c06abb3181eedca690170f7ed0fd26a70bfacc", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/6ce453073a0c220b254dd7f4383645615e4071c3", + "reference": "6ce453073a0c220b254dd7f4383645615e4071c3", "shasum": "" }, "require": { - "php": ">=7.2", - "phpunit/phpunit": "^9.0" + "php": "^7.4 | ^8.0", + "phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | ^11 | ^12" + }, + "conflict": { + "codeception/codeception": "<5.0.6" }, "require-dev": { - "codeception/specify": "*", - "consolidation/robo": "^3.0.0-alpha3", - "vlucas/phpdotenv": "^3.0" + "consolidation/robo": "^3.0" }, "type": "library", "autoload": { "psr-4": { - "Codeception\\PHPUnit\\": "src/" + "Codeception\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Davert", - "email": "davert.php@resend.cc" - }, - { - "name": "Naktibalda" - } - ], - "description": "PHPUnit classes used by Codeception", + "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", "support": { - "issues": "https://github.com/Codeception/phpunit-wrapper/issues", - "source": "https://github.com/Codeception/phpunit-wrapper/tree/9.0.6" + "issues": "https://github.com/Codeception/Stub/issues", + "source": "https://github.com/Codeception/Stub/tree/4.1.4" }, - "time": "2020-12-28T13:59:47+00:00" + "time": "2025-02-14T06:56:33+00:00" }, { - "name": "codeception/stub", - "version": "3.7.0", + "name": "composer/ca-bundle", + "version": "1.5.6", "source": { "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "468dd5fe659f131fc997f5196aad87512f9b1304" + "url": "https://github.com/composer/ca-bundle.git", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/468dd5fe659f131fc997f5196aad87512f9b1304", - "reference": "468dd5fe659f131fc997f5196aad87512f9b1304", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f65c239c970e7f072f067ab78646e9f0b2935175", + "reference": "f65c239c970e7f072f067ab78646e9f0b2935175", "shasum": "" }, "require": { - "phpunit/phpunit": "^8.4 | ^9.0" + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "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-main": "1.x-dev" + } + }, "autoload": { "psr-4": { - "Codeception\\": "src/" + "Composer\\CaBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], "support": { - "issues": "https://github.com/Codeception/Stub/issues", - "source": "https://github.com/Codeception/Stub/tree/3.7.0" + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.6" }, - "time": "2020-07-03T15:54:43+00:00" + "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-03-06T14:30:56+00:00" }, { - "name": "composer/ca-bundle", - "version": "1.2.9", + "name": "composer/class-map-generator", + "version": "1.6.1", "source": { "type": "git", - "url": "https://github.com/composer/ca-bundle.git", - "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5" + "url": "https://github.com/composer/class-map-generator.git", + "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/78a0e288fdcebf92aa2318a8d3656168da6ac1a5", - "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/134b705ddb0025d397d8318a75825fe3c9d1da34", + "reference": "134b705ddb0025d397d8318a75825fe3c9d1da34", "shasum": "" }, "require": { - "ext-openssl": "*", - "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" + "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" }, "type": "library", "extra": { @@ -824,7 +921,7 @@ }, "autoload": { "psr-4": { - "Composer\\CaBundle\\": "src" + "Composer\\ClassMapGenerator\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -835,21 +932,16 @@ { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "homepage": "https://seld.be" } ], - "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "description": "Utilities to scan PHP code and generate class maps.", "keywords": [ - "cabundle", - "cacert", - "certificate", - "ssl", - "tls" + "classmap" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.9" + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.6.1" }, "funding": [ { @@ -865,42 +957,52 @@ "type": "tidelift" } ], - "time": "2021-01-12T12:10:35+00:00" + "time": "2025-03-24T13:50:44+00:00" }, { "name": "composer/composer", - "version": "2.0.13", + "version": "2.8.8", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "986e8b86b7b570632ad0a905c3726c33dd4c0efb" + "reference": "85ff84d6c5260ba21740a7c5c9a111890805d6e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/986e8b86b7b570632ad0a905c3726c33dd4c0efb", - "reference": "986e8b86b7b570632ad0a905c3726c33dd4c0efb", + "url": "https://api.github.com/repos/composer/composer/zipball/85ff84d6c5260ba21740a7c5c9a111890805d6e7", + "reference": "85ff84d6c5260ba21740a7c5c9a111890805d6e7", "shasum": "" }, "require": { - "composer/ca-bundle": "^1.0", + "composer/ca-bundle": "^1.5", + "composer/class-map-generator": "^1.4.0", "composer/metadata-minifier": "^1.0", - "composer/semver": "^3.0", - "composer/spdx-licenses": "^1.2", - "composer/xdebug-handler": "^1.1", - "justinrainbow/json-schema": "^5.2.10", - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0", - "react/promise": "^1.2 || ^2.7", + "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": "^6.3.1", + "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.0", - "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", - "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", - "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0", - "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0" + "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": { - "phpspec/prophecy": "^1.10", - "symfony/phpunit-bridge": "^4.2 || ^5.0" + "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", @@ -912,13 +1014,18 @@ ], "type": "library", "extra": { + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] + }, "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "2.8-dev" } }, "autoload": { "psr-4": { - "Composer\\": "src/Composer" + "Composer\\": "src/Composer/" } }, "notification-url": "https://packagist.org/downloads/", @@ -945,9 +1052,10 @@ "package" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.0.13" + "security": "https://github.com/composer/composer/security/policy", + "source": "https://github.com/composer/composer/tree/2.8.8" }, "funding": [ { @@ -963,7 +1071,7 @@ "type": "tidelift" } ], - "time": "2021-04-27T11:11:08+00:00" + "time": "2025-04-04T14:56:46+00:00" }, { "name": "composer/metadata-minifier", @@ -1035,25 +1143,104 @@ "time": "2021-04-07T13:37:33+00:00" }, { - "name": "composer/semver", - "version": "3.2.4", + "name": "composer/pcre", + "version": "3.3.2", "source": { "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464" + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", - "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464", + "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": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.54", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -1095,9 +1282,9 @@ "versioning" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.4" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -1113,27 +1300,28 @@ "type": "tidelift" } ], - "time": "2020-11-13T08:59:24+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/spdx-licenses", - "version": "1.5.5", + "version": "1.5.8", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "de30328a7af8680efdc03e396aad24befd513200" + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/de30328a7af8680efdc03e396aad24befd513200", - "reference": "de30328a7af8680efdc03e396aad24befd513200", + "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": { @@ -1174,9 +1362,9 @@ "validator" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "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.5" + "source": "https://github.com/composer/spdx-licenses/tree/1.5.8" }, "funding": [ { @@ -1192,29 +1380,31 @@ "type": "tidelift" } ], - "time": "2020-12-03T16:04:16+00:00" + "time": "2023-11-20T07:44:33+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.6", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "f27e06cd9675801df441b3656569b328e04aa37c" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", - "reference": "f27e06cd9675801df441b3656569b328e04aa37c", + "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": { - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -1238,9 +1428,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/1.4.6" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -1256,30 +1446,30 @@ "type": "tidelift" } ], - "time": "2021-03-25T17:01:18+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "csharpru/vault-php", - "version": "4.2.1", + "version": "4.4.0", "source": { "type": "git", "url": "https://github.com/CSharpRU/vault-php.git", - "reference": "89b393ecf65f61a44d3a1872547f65085982b481" + "reference": "ba4cbd7b55f1ce10bc72a7e4c36cfd87a42d3573" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CSharpRU/vault-php/zipball/89b393ecf65f61a44d3a1872547f65085982b481", - "reference": "89b393ecf65f61a44d3a1872547f65085982b481", + "url": "https://api.github.com/repos/CSharpRU/vault-php/zipball/ba4cbd7b55f1ce10bc72a7e4c36cfd87a42d3573", + "reference": "ba4cbd7b55f1ce10bc72a7e4c36cfd87a42d3573", "shasum": "" }, "require": { + "aws/aws-sdk-php": "^3.0", "ext-json": "*", "php": "^7.2 || ^8.0", - "psr/cache": "^1.0", + "psr/cache": "^1.0|^2.0|^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/log": "^1.0", - "weew/helpers-array": "^1.3" + "psr/log": "^1.0|^2.0|^3.0" }, "require-dev": { "alextartan/guzzle-psr18-adapter": "^1.2 || ^2.0", @@ -1317,78 +1507,40 @@ ], "support": { "issues": "https://github.com/CSharpRU/vault-php/issues", - "source": "https://github.com/CSharpRU/vault-php/tree/4.2.1" - }, - "time": "2021-05-21T06:39:35+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" - } - ], - "description": "Guzzle6 transport for Vault PHP client", - "support": { - "issues": "https://github.com/CSharpRU/vault-php-guzzle6-transport/issues", - "source": "https://github.com/CSharpRU/vault-php-guzzle6-transport/tree/master" + "source": "https://github.com/CSharpRU/vault-php/tree/4.4.0" }, - "time": "2019-03-10T06:17:37+00:00" + "time": "2023-11-22T11:38:41+00:00" }, { "name": "doctrine/annotations", - "version": "1.13.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f" + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f", - "reference": "e6e7b7d5b45a2f2abc5460cc6396480b2b1d321f", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", "shasum": "" }, "require": { - "doctrine/lexer": "1.*", + "doctrine/lexer": "^2 || ^3", "ext-tokenizer": "*", - "php": "^7.1 || ^8.0", + "php": "^7.2 || ^8.0", "psr/cache": "^1 || ^2 || ^3" }, "require-dev": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", - "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" + "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" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" }, "type": "library", "autoload": { @@ -1431,110 +1583,38 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.1" - }, - "time": "2021-05-16T18:07:53+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^8.0", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" - }, - "type": "library", - "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": "https://ocramius.github.io/" - } - ], - "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" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + "source": "https://github.com/doctrine/annotations/tree/2.0.2" }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2024-09-05T10:17:24+00:00" }, { "name": "doctrine/lexer", - "version": "1.2.1", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "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.2.x-dev" - } - }, "autoload": { "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + "Doctrine\\Common\\Lexer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1566,7 +1646,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.1" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -1582,122 +1662,191 @@ "type": "tidelift" } ], - "time": "2020-05-25T17:44:05+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "6.5.5", + "version": "7.9.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", - "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.6.1", - "php": ">=5.5", - "symfony/polyfill-intl-idn": "^1.17.0" + "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" ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/6.5" + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" }, - "time": "2020-06-16T21:01:06+00:00" + "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": "2025-03-27T13:37:11+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.4.1", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4 || ^5.1" + "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", @@ -1706,66 +1855,107 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.1" + "source": "https://github.com/guzzle/promises/tree/2.2.0" }, - "time": "2021-03-07T09:25:29+00:00" + "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": "2025-03-27T13:27:01+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.8.2", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", - "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", "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.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.7-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", @@ -1781,196 +1971,225 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.2" + "source": "https://github.com/guzzle/psr7/tree/2.7.1" }, - "time": "2021-04-26T09:17:50+00:00" + "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": "2025-03-27T12:30:47+00:00" }, { - "name": "hoa/consistency", - "version": "1.17.05.02", + "name": "justinrainbow/json-schema", + "version": "6.4.1", "source": { "type": "git", - "url": "https://github.com/hoaproject/Consistency.git", - "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "35d262c94959571e8736db1e5c9bc36ab94ae900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", - "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/35d262c94959571e8736db1e5c9bc36ab94ae900", + "reference": "35d262c94959571e8736db1e5c9bc36ab94ae900", "shasum": "" }, "require": { - "hoa/exception": "~1.0", - "php": ">=5.5.0" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "hoa/stream": "~1.0", - "hoa/test": "~2.0" + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "1.2.0", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, + "bin": [ + "bin/validate-json" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "6.x-dev" } }, "autoload": { "psr-4": { - "Hoa\\Consistency\\": "." - }, - "files": [ - "Prelude.php" - ] + "JsonSchema\\": "src/JsonSchema/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" + "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": "Hoa community", - "homepage": "https://hoa-project.net/" + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" } ], - "description": "The Hoa\\Consistency library.", - "homepage": "https://hoa-project.net/", + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ - "autoloader", - "callable", - "consistency", - "entity", - "flex", - "keyword", - "library" + "json", + "schema" ], "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Consistency", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Consistency/issues", - "source": "https://central.hoa-project.net/Resource/Library/Consistency" - }, - "time": "2017-05-02T12:18:12+00:00" + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.1" + }, + "time": "2025-04-04T13:08:07+00:00" }, { - "name": "hoa/console", - "version": "3.17.05.02", + "name": "laminas/laminas-diactoros", + "version": "3.5.0", "source": { "type": "git", - "url": "https://github.com/hoaproject/Console.git", - "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66" + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Console/zipball/e231fd3ea70e6d773576ae78de0bdc1daf331a66", - "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/143a16306602ce56b8b092a7914fef03c37f9ed2", + "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2", "shasum": "" }, "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/file": "~1.0", - "hoa/protocol": "~1.0", - "hoa/stream": "~1.0", - "hoa/ustring": "~4.0" + "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" }, - "require-dev": { - "hoa/test": "~2.0" + "conflict": { + "amphp/amp": "<2.6.4" }, - "suggest": { - "ext-pcntl": "To enable hoa://Event/Console/Window:resize.", - "hoa/dispatcher": "To use the console kit.", - "hoa/router": "To use the console kit." + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "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" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.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": { - "Hoa\\Console\\": "." + "Laminas\\Diactoros\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Console library.", - "homepage": "https://hoa-project.net/", + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", "keywords": [ - "autocompletion", - "chrome", - "cli", - "console", - "cursor", - "getoption", - "library", - "option", - "parser", - "processus", - "readline", - "terminfo", - "tput", - "window" + "http", + "laminas", + "psr", + "psr-17", + "psr-7" ], "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Console", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Console/issues", - "source": "https://central.hoa-project.net/Resource/Library/Console" - }, - "time": "2017-05-02T12:26:19+00:00" + "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" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2024-10-14T11:59:49+00:00" }, { - "name": "hoa/event", - "version": "1.17.01.13", + "name": "marc-mabe/php-enum", + "version": "v4.7.1", "source": { "type": "git", - "url": "https://github.com/hoaproject/Event.git", - "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", - "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", "shasum": "" }, "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" + "ext-reflection": "*", + "php": "^7.1 | ^8.0" }, "require-dev": { - "hoa/test": "~2.0" + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" } }, "autoload": { "psr-4": { - "Hoa\\Event\\": "." - } + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1978,326 +2197,349 @@ ], "authors": [ { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" } ], - "description": "The Hoa\\Event library.", - "homepage": "https://hoa-project.net/", + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", "keywords": [ - "event", - "library", - "listener", - "observer" + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" ], "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Event", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Event/issues", - "source": "https://central.hoa-project.net/Resource/Library/Event" - }, - "time": "2017-01-13T15:30:50+00:00" + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "time": "2024-11-28T04:54:44+00:00" }, { - "name": "hoa/exception", - "version": "1.17.01.16", + "name": "monolog/monolog", + "version": "3.9.0", "source": { "type": "git", - "url": "https://github.com/hoaproject/Exception.git", - "reference": "091727d46420a3d7468ef0595651488bfc3a458f" + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", - "reference": "091727d46420a3d7468ef0595651488bfc3a458f", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", "shasum": "" }, "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0" + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" }, "require-dev": { - "hoa/test": "~2.0" + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "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-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 library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { "psr-4": { - "Hoa\\Exception\\": "." + "Monolog\\": "src/Monolog" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" } ], - "description": "The Hoa\\Exception library.", - "homepage": "https://hoa-project.net/", + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", "keywords": [ - "exception", - "library" + "log", + "logging", + "psr-3" ], "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Exception", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Exception/issues", - "source": "https://central.hoa-project.net/Resource/Library/Exception" - }, - "time": "2017-01-16T07:53:27+00:00" + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" }, { - "name": "hoa/file", - "version": "1.17.07.11", + "name": "mtdowling/jmespath.php", + "version": "2.8.0", "source": { "type": "git", - "url": "https://github.com/hoaproject/File.git", - "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", - "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", "shasum": "" }, "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/iterator": "~2.0", - "hoa/stream": "~1.0" + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" }, "require-dev": { - "hoa/test": "~2.0" + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" }, + "bin": [ + "bin/jp.php" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "2.8-dev" } }, "autoload": { + "files": [ + "src/JmesPath.php" + ], "psr-4": { - "Hoa\\File\\": "." + "JmesPath\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" }, { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" } ], - "description": "The Hoa\\File library.", - "homepage": "https://hoa-project.net/", + "description": "Declaratively specify how to extract elements from a JSON document", "keywords": [ - "Socket", - "directory", - "file", - "finder", - "library", - "link", - "temporary" + "json", + "jsonpath" ], "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/File", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/File/issues", - "source": "https://central.hoa-project.net/Resource/Library/File" - }, - "time": "2017-07-11T07:42:15+00:00" + "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": "hoa/iterator", - "version": "2.17.01.10", + "name": "mustache/mustache", + "version": "v2.14.2", "source": { "type": "git", - "url": "https://github.com/hoaproject/Iterator.git", - "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" + "url": "https://github.com/bobthecow/mustache.php.git", + "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", - "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e62b7c3849d22ec55f3ec425507bf7968193a6cb", + "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb", "shasum": "" }, "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" + "php": ">=5.2.4" }, "require-dev": { - "hoa/test": "~2.0" + "friendsofphp/php-cs-fixer": "~1.11", + "phpunit/phpunit": "~3.7|~4.0|~5.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { - "psr-4": { - "Hoa\\Iterator\\": "." + "psr-0": { + "Mustache": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" } ], - "description": "The Hoa\\Iterator library.", - "homepage": "https://hoa-project.net/", + "description": "A Mustache implementation in PHP.", + "homepage": "https://github.com/bobthecow/mustache.php", "keywords": [ - "iterator", - "library" + "mustache", + "templating" ], "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Iterator", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Iterator/issues", - "source": "https://central.hoa-project.net/Resource/Library/Iterator" - }, - "time": "2017-01-10T10:34:47+00:00" + "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": "hoa/protocol", - "version": "1.17.01.14", + "name": "myclabs/deep-copy", + "version": "1.13.0", "source": { "type": "git", - "url": "https://github.com/hoaproject/Protocol.git", - "reference": "5c2cf972151c45f373230da170ea015deecf19e2" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", - "reference": "5c2cf972151c45f373230da170ea015deecf19e2", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "hoa/test": "~2.0" + "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", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { - "psr-4": { - "Hoa\\Protocol\\": "." - }, "files": [ - "Wrapper.php" - ] + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } + "MIT" ], - "description": "The Hoa\\Protocol library.", - "homepage": "https://hoa-project.net/", + "description": "Create deep copies (clones) of your objects", "keywords": [ - "library", - "protocol", - "resource", - "stream", - "wrapper" + "clone", + "copy", + "duplicate", + "object", + "object graph" ], "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Protocol", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Protocol/issues", - "source": "https://central.hoa-project.net/Resource/Library/Protocol" - }, - "time": "2017-01-14T12:26:10+00:00" + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-02-12T12:17:51+00:00" }, { - "name": "hoa/stream", - "version": "1.17.02.21", + "name": "nikic/php-parser", + "version": "v5.4.0", "source": { "type": "git", - "url": "https://github.com/hoaproject/Stream.git", - "reference": "3293cfffca2de10525df51436adf88a559151d82" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", - "reference": "3293cfffca2de10525df51436adf88a559151d82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/protocol": "~1.0" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "hoa/test": "~2.0" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, + "bin": [ + "bin/php-parse" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "5.0-dev" } }, "autoload": { "psr-4": { - "Hoa\\Stream\\": "." + "PhpParser\\": "lib/PhpParser" } }, "notification-url": "https://packagist.org/downloads/", @@ -2306,745 +2548,113 @@ ], "authors": [ { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" + "name": "Nikita Popov" } ], - "description": "The Hoa\\Stream library.", - "homepage": "https://hoa-project.net/", + "description": "A PHP parser written in PHP", "keywords": [ - "Context", - "bucket", - "composite", - "filter", - "in", - "library", - "out", - "protocol", - "stream", - "wrapper" + "parser", + "php" ], "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Stream", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Stream/issues", - "source": "https://central.hoa-project.net/Resource/Library/Stream" - }, - "time": "2017-02-21T16:01:06+00:00" + "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": "hoa/ustring", - "version": "4.17.01.16", + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", "source": { "type": "git", - "url": "https://github.com/hoaproject/Ustring.git", - "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", - "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", "shasum": "" }, "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" + "php": "^8" }, "require-dev": { - "hoa/test": "~2.0" - }, - "suggest": { - "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", - "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, "autoload": { "psr-4": { - "Hoa\\Ustring\\": "." + "ParagonIE\\ConstantTime\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" }, { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" } ], - "description": "The Hoa\\Ustring library.", - "homepage": "https://hoa-project.net/", + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", "keywords": [ - "library", - "search", - "string", - "unicode" - ], - "support": { - "docs": "https://central.hoa-project.net/Documentation/Library/Ustring", - "email": "support@hoa-project.net", - "forum": "https://users.hoa-project.net/", - "irc": "irc://chat.freenode.net/hoaproject", - "issues": "https://github.com/hoaproject/Ustring/issues", - "source": "https://central.hoa-project.net/Resource/Library/Ustring" - }, - "time": "2017-01-16T07:08:25+00:00" - }, - { - "name": "jms/metadata", - "version": "2.5.0", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/metadata.git", - "reference": "b5c52549807b2d855b3d7e36ec164c00eb547338" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/b5c52549807b2d855b3d7e36ec164c00eb547338", - "reference": "b5c52549807b2d855b3d7e36ec164c00eb547338", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0" - }, - "require-dev": { - "doctrine/cache": "^1.0", - "doctrine/coding-standard": "^8.0", - "mikey179/vfsstream": "^1.6.7", - "phpunit/phpunit": "^8.5|^9.0", - "psr/container": "^1.0", - "symfony/cache": "^3.1|^4.0|^5.0", - "symfony/dependency-injection": "^3.1|^4.0|^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Metadata\\": "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": "Class/method/property metadata management in PHP", - "keywords": [ - "annotations", - "metadata", - "xml", - "yaml" - ], - "support": { - "issues": "https://github.com/schmittjoh/metadata/issues", - "source": "https://github.com/schmittjoh/metadata/tree/2.5.0" - }, - "time": "2021-03-07T19:20:09+00:00" - }, - { - "name": "jms/serializer", - "version": "3.12.3", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/serializer.git", - "reference": "f908d17afd08f0aab9c083322022682b41483c5b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/f908d17afd08f0aab9c083322022682b41483c5b", - "reference": "f908d17afd08f0aab9c083322022682b41483c5b", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.0", - "doctrine/instantiator": "^1.0.3", - "doctrine/lexer": "^1.1", - "jms/metadata": "^2.0", - "php": "^7.2||^8.0", - "phpstan/phpdoc-parser": "^0.4 || ^0.5" - }, - "require-dev": { - "doctrine/coding-standard": "^8.1", - "doctrine/orm": "~2.1", - "doctrine/persistence": "^1.3.3|^2.0|^3.0", - "doctrine/phpcr-odm": "^1.3|^2.0", - "ext-pdo_sqlite": "*", - "jackalope/jackalope-doctrine-dbal": "^1.1.5", - "ocramius/proxy-manager": "^1.0|^2.0", - "phpstan/phpstan": "^0.12.65", - "phpunit/phpunit": "^8.0||^9.0", - "psr/container": "^1.0", - "symfony/dependency-injection": "^3.0|^4.0|^5.0", - "symfony/expression-language": "^3.0|^4.0|^5.0", - "symfony/filesystem": "^3.0|^4.0|^5.0", - "symfony/form": "^3.0|^4.0|^5.0", - "symfony/translation": "^3.0|^4.0|^5.0", - "symfony/validator": "^3.1.9|^4.0|^5.0", - "symfony/yaml": "^3.3|^4.0|^5.0", - "twig/twig": "~1.34|~2.4|^3.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 use the YAML metadata format." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.12-dev" - } - }, - "autoload": { - "psr-4": { - "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" - ], - "support": { - "issues": "https://github.com/schmittjoh/serializer/issues", - "source": "https://github.com/schmittjoh/serializer/tree/3.12.3" - }, - "funding": [ - { - "url": "https://github.com/goetas", - "type": "github" - } - ], - "time": "2021-04-25T11:03:24+00:00" - }, - { - "name": "justinrainbow/json-schema", - "version": "5.2.10", - "source": { - "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", - "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "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" - }, - "bin": [ - "bin/validate-json" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "JsonSchema\\": "src/JsonSchema/" - } - }, - "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" - } - ], - "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", - "keywords": [ - "json", - "schema" - ], - "support": { - "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/5.2.10" - }, - "time": "2020-05-27T16:41:55+00:00" - }, - { - "name": "monolog/monolog", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1cb1cde8e8dd0f70cc0fe51354a59acad9302084", - "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "psr/log": "^1.0.1" - }, - "provide": { - "psr/log-implementation": "1.0.0" - }, - "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", - "doctrine/couchdb": "~1.0@dev", - "elasticsearch/elasticsearch": "^7", - "graylog2/gelf-php": "^1.4.2", - "mongodb/mongodb": "^1.8", - "php-amqplib/php-amqplib": "~2.4", - "php-console/php-console": "^3.1.3", - "phpspec/prophecy": "^1.6.1", - "phpstan/phpstan": "^0.12.59", - "phpunit/phpunit": "^8.5", - "predis/predis": "^1.1", - "rollbar/rollbar": "^1.3", - "ruflin/elastica": ">=0.90 <7.0.1", - "swiftmailer/swiftmailer": "^5.3|^6.0" - }, - "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-mbstring": "Allow to work properly with unicode symbols", - "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", - "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "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" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Monolog\\": "src/Monolog" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "https://seld.be" - } - ], - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "https://github.com/Seldaek/monolog", - "keywords": [ - "log", - "logging", - "psr-3" - ], - "support": { - "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.2.0" - }, - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", - "type": "tidelift" - } - ], - "time": "2020-12-14T13:15:25+00:00" - }, - { - "name": "mtdowling/jmespath.php", - "version": "2.6.0", - "source": { - "type": "git", - "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/42dae2cbd13154083ca6d70099692fef8ca84bfb", - "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb", - "shasum": "" - }, - "require": { - "php": "^5.4 || ^7.0 || ^8.0", - "symfony/polyfill-mbstring": "^1.17" - }, - "require-dev": { - "composer/xdebug-handler": "^1.4", - "phpunit/phpunit": "^4.8.36 || ^7.5.15" - }, - "bin": [ - "bin/jp.php" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-dev" - } - }, - "autoload": { - "psr-4": { - "JmesPath\\": "src/" - }, - "files": [ - "src/JmesPath.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Declaratively specify how to extract elements from a JSON document", - "keywords": [ - "json", - "jsonpath" - ], - "support": { - "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/2.6.0" - }, - "time": "2020-07-31T21:01:56+00:00" - }, - { - "name": "mustache/mustache", - "version": "v2.13.0", - "source": { - "type": "git", - "url": "https://github.com/bobthecow/mustache.php.git", - "reference": "e95c5a008c23d3151d59ea72484d4f72049ab7f4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e95c5a008c23d3151d59ea72484d4f72049ab7f4", - "reference": "e95c5a008c23d3151d59ea72484d4f72049ab7f4", - "shasum": "" - }, - "require": { - "php": ">=5.2.4" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~1.11", - "phpunit/phpunit": "~3.7|~4.0|~5.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Mustache": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" - } - ], - "description": "A Mustache implementation in PHP.", - "homepage": "https://github.com/bobthecow/mustache.php", - "keywords": [ - "mustache", - "templating" - ], - "support": { - "issues": "https://github.com/bobthecow/mustache.php/issues", - "source": "https://github.com/bobthecow/mustache.php/tree/master" - }, - "time": "2019-11-23T21:40:31+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.10.2", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, - "files": [ - "src/DeepCopy/deep_copy.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2020-11-13T09:40:50+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.6.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "c346bbfafe2ff60680258b631afb730d186ed864" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c346bbfafe2ff60680258b631afb730d186ed864", - "reference": "c346bbfafe2ff60680258b631afb730d186ed864", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "0.0.5", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-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/v4.6.0" - }, - "time": "2020-07-02T17:12:47+00:00" - }, - { - "name": "paragonie/constant_time_encoding", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "f34c2b11eb9d2c9318e13540a1dbc2a3afbd939c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/f34c2b11eb9d2c9318e13540a1dbc2a3afbd939c", - "reference": "f34c2b11eb9d2c9318e13540a1dbc2a3afbd939c", - "shasum": "" - }, - "require": { - "php": "^7|^8" - }, - "require-dev": { - "phpunit/phpunit": "^6|^7|^8|^9", - "vimeo/psalm": "^1|^2|^3|^4" - }, - "type": "library", - "autoload": { - "psr-4": { - "ParagonIE\\ConstantTime\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@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": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", - "keywords": [ - "base16", - "base32", - "base32_decode", - "base32_encode", - "base64", - "base64_decode", - "base64_encode", - "bin2hex", - "encoding", - "hex", - "hex2bin", - "rfc4648" + "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": "2020-12-06T15:14:20+00:00" + "time": "2024-05-08T12:36:18+00:00" }, { "name": "phar-io/manifest", - "version": "1.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", - "phar-io/version": "^2.0", - "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": { @@ -3076,26 +2686,32 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2018-07-08T19:23:20+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", - "version": "2.0.1", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "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": { @@ -3127,388 +2743,116 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/master" + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2018-07-08T19:19:57+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { "name": "php-webdriver/webdriver", - "version": "1.11.1", + "version": "1.15.2", "source": { "type": "git", "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "da16e39968f8dd5cfb7d07eef91dc2b731c69880" + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/da16e39968f8dd5cfb7d07eef91dc2b731c69880", - "reference": "da16e39968f8dd5cfb7d07eef91dc2b731c69880", + "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 || ^8.0", - "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0" - }, - "replace": { - "facebook/webdriver": "*" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.0", - "ondram/ci-detector": "^2.1 || ^3.5 || ^4.0", - "php-coveralls/php-coveralls": "^2.4", - "php-mock/php-mock-phpunit": "^1.1 || ^2.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpunit/phpunit": "^5.7 || ^7 || ^8 || ^9", - "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^3.3 || ^4.0 || ^5.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - }, - "files": [ - "lib/Exception/TimeoutException.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", - "homepage": "https://github.com/php-webdriver/php-webdriver", - "keywords": [ - "Chromedriver", - "geckodriver", - "php", - "selenium", - "webdriver" - ], - "support": { - "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.11.1" - }, - "time": "2021-05-21T15:12:49+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" - }, - "time": "2020-09-03T19:13:55+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" - }, - "time": "2020-09-17T18:55:26+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.13.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" - }, - "time": "2021-03-17T13:42:18+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "0.5.4", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "e352d065af1ae9b41c12d1dfd309e90f7b1f55c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/e352d065af1ae9b41c12d1dfd309e90f7b1f55c9", - "reference": "e352d065af1ae9b41c12d1dfd309e90f7b1f55c9", - "shasum": "" + "ext-json": "*", + "ext-zip": "*", + "php": "^7.3 || ^8.0", + "symfony/polyfill-mbstring": "^1.12", + "symfony/process": "^5.0 || ^6.0 || ^7.0" }, - "require": { - "php": "^7.1 || ^8.0" + "replace": { + "facebook/webdriver": "*" }, "require-dev": { - "phing/phing": "^2.16.3", + "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", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.60", - "phpstan/phpstan-strict-rules": "^0.12.5", - "phpunit/phpunit": "^7.5.20", - "symfony/process": "^5.2" + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.5-dev" - } + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" }, + "type": "library", "autoload": { + "files": [ + "lib/Exception/TimeoutException.php" + ], "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] + "Facebook\\WebDriver\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", + "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", + "homepage": "https://github.com/php-webdriver/php-webdriver", + "keywords": [ + "Chromedriver", + "geckodriver", + "php", + "selenium", + "webdriver" + ], "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/0.5.4" + "issues": "https://github.com/php-webdriver/php-webdriver/issues", + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" }, - "time": "2021-04-03T14:46:19+00:00" + "time": "2024-11-21T15:12:59+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "8.0.2", + "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc" + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca6647ffddd2add025ab3f21644a441d7c146cdc", - "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-xmlwriter": "*", - "php": "^7.3", - "phpunit/php-file-iterator": "^3.0", - "phpunit/php-text-template": "^2.0", - "phpunit/php-token-stream": "^4.0", - "sebastian/code-unit-reverse-lookup": "^2.0", - "sebastian/environment": "^5.0", - "sebastian/version": "^3.0", - "theseer/tokenizer": "^1.1.3" + "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" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^10.1" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "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": "8.0-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -3536,7 +2880,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/8.0.2" + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -3544,32 +2889,32 @@ "type": "github" } ], - "time": "2020-05-23T08:02:54+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.5", + "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3596,7 +2941,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, "funding": [ { @@ -3604,28 +2950,28 @@ "type": "github" } ], - "time": "2020-09-28T05:57:25+00:00" + "time": "2023-08-31T06:24:48+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "suggest": { "ext-pcntl": "*" @@ -3633,7 +2979,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -3659,7 +3005,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" }, "funding": [ { @@ -3667,32 +3013,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2023-02-03T06:56:09+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -3718,7 +3064,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" }, "funding": [ { @@ -3726,32 +3073,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2023-08-31T14:07:24+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3777,66 +3124,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:16:10+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", - "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.3 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-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" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", - "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" }, "funding": [ { @@ -3844,59 +3132,52 @@ "type": "github" } ], - "abandoned": true, - "time": "2020-08-04T08:28:15+00:00" + "time": "2023-02-03T06:57:52+00:00" }, { "name": "phpunit/phpunit", - "version": "9.2.6", + "version": "10.5.45", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "1c6a9e4312e209e659f1fce3ce88dd197c2448f6" + "reference": "bd68a781d8e30348bc297449f5234b3458267ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c6a9e4312e209e659f1fce3ce88dd197c2448f6", - "reference": "1c6a9e4312e209e659f1fce3ce88dd197c2448f6", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8", + "reference": "bd68a781d8e30348bc297449f5234b3458267ae8", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.9.5", - "phar-io/manifest": "^1.0.3", - "phar-io/version": "^2.0.1", - "php": "^7.3", - "phpspec/prophecy": "^1.10.3", - "phpunit/php-code-coverage": "^8.0.2", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-invoker": "^3.0.2", - "phpunit/php-text-template": "^2.0.2", - "phpunit/php-timer": "^5.0.1", - "sebastian/code-unit": "^1.0.5", - "sebastian/comparator": "^4.0.3", - "sebastian/diff": "^4.0.1", - "sebastian/environment": "^5.1.2", - "sebastian/exporter": "^4.0.2", - "sebastian/global-state": "^4.0", - "sebastian/object-enumerator": "^4.0.2", - "sebastian/resource-operations": "^3.0.2", - "sebastian/type": "^2.1.1", - "sebastian/version": "^3.0.1" - }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.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-soap": "*", - "ext-xdebug": "*" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -3904,15 +3185,15 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "10.5-dev" } }, "autoload": { - "classmap": [ - "src/" - ], "files": [ "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3935,36 +3216,41 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.2.6" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45" }, "funding": [ { - "url": "https://phpunit.de/donate.html", + "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": "2020-07-13T17:55:55+00:00" + "time": "2025-02-06T16:08:12+00:00" }, { "name": "psr/cache", - "version": "1.0.1", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { @@ -3984,7 +3270,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for caching libraries", @@ -3994,28 +3280,81 @@ "psr-6" ], "support": { - "source": "https://github.com/php-fig/cache/tree/master" + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2016-08-06T20:24:11+00:00" + "time": "2022-11-25T14:36:26+00:00" }, { "name": "psr/container", - "version": "1.1.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -4042,27 +3381,77 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "time": "2021-03-05T17:36:06+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { "name": "psr/http-client", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/php-fig/http-client.git", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", - "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -4082,7 +3471,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP clients", @@ -4094,27 +3483,27 @@ "psr-18" ], "support": { - "source": "https://github.com/php-fig/http-client/tree/master" + "source": "https://github.com/php-fig/http-client" }, - "time": "2020-06-29T06:28:15+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { "name": "psr/http-factory", - "version": "1.0.1", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", - "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=7.0.0", - "psr/http-message": "^1.0" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -4134,10 +3523,10 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -4149,31 +3538,31 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2019-04-30T12:38:16+00:00" + "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": { @@ -4188,7 +3577,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -4202,36 +3591,36 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/master" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2016-08-06T14:39:51+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/log", - "version": "1.1.4", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "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/", @@ -4252,9 +3641,88 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "source": "https://github.com/php-fig/log/tree/2.0.0" + }, + "time": "2021-07-14T16:41:46+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.8", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85057ceedee50c49d4f6ecaff73ee96adb3b3625", + "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625", + "shasum": "" + }, + "require": { + "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-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.8" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2025-03-16T03:05:19+00:00" }, { "name": "ralouphie/getallheaders", @@ -4302,40 +3770,49 @@ }, { "name": "ramsey/collection", - "version": "1.1.3", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1" + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1", - "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { - "php": "^7.2 || ^8" + "php": "^8.1" }, "require-dev": { - "captainhook/captainhook": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "ergebnis/composer-normalize": "^2.6", - "fakerphp/faker": "^1.5", - "hamcrest/hamcrest-php": "^2", - "jangregor/phpstan-prophecy": "^0.8", - "mockery/mockery": "^1.3", - "phpstan/extension-installer": "^1", - "phpstan/phpstan": "^0.12.32", - "phpstan/phpstan-mockery": "^0.12.5", - "phpstan/phpstan-phpunit": "^0.12.11", - "phpunit/phpunit": "^8.5 || ^9", - "psy/psysh": "^0.10.4", - "slevomat/coding-standard": "^6.3", - "squizlabs/php_codesniffer": "^3.5", - "vimeo/psalm": "^4.4" + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, "autoload": { "psr-4": { "Ramsey\\Collection\\": "src/" @@ -4352,80 +3829,68 @@ "homepage": "https://benramsey.com" } ], - "description": "A PHP 7.2+ library for representing and manipulating collections.", + "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/1.1.3" - }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" - } + "set" ], - "time": "2021-01-21T17:40:04+00:00" + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" }, { "name": "ramsey/uuid", - "version": "4.1.1", + "version": "4.7.6", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "cd4032040a750077205918c86049aa0f43d22947" + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/cd4032040a750077205918c86049aa0f43d22947", - "reference": "cd4032040a750077205918c86049aa0f43d22947", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", "shasum": "" }, "require": { - "brick/math": "^0.8 || ^0.9", + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", "ext-json": "*", - "php": "^7.2 || ^8", - "ramsey/collection": "^1.0", - "symfony/polyfill-ctype": "^1.8" + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" }, "replace": { "rhumsaa/uuid": "self.version" }, "require-dev": { - "codeception/aspect-mock": "^3", - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7.0", + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "doctrine/annotations": "^1.8", - "goaop/framework": "^2", + "ergebnis/composer-normalize": "^2.15", "mockery/mockery": "^1.3", - "moontoast/math": "^1.1", "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", "php-mock/php-mock-mockery": "^1.3", - "php-mock/php-mock-phpunit": "^2.5", "php-parallel-lint/php-parallel-lint": "^1.1", - "phpbench/phpbench": "^0.17.1", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-mockery": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^8.5", - "psy/psysh": "^0.10.0", - "slevomat/coding-standard": "^6.0", + "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": "3.9.4" + "vimeo/psalm": "^4.9" }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", - "ext-ctype": "Enables faster processing of character classification using ctype functions.", "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", @@ -4433,24 +3898,23 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "4.x-dev" + "captainhook": { + "force-install": true } }, "autoload": { - "psr-4": { - "Ramsey\\Uuid\\": "src/" - }, "files": [ "src/functions.php" - ] + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", - "homepage": "https://github.com/ramsey/uuid", "keywords": [ "guid", "identifier", @@ -4458,45 +3922,49 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "rss": "https://github.com/ramsey/uuid/releases.atom", - "source": "https://github.com/ramsey/uuid" + "source": "https://github.com/ramsey/uuid/tree/4.7.6" }, "funding": [ { "url": "https://github.com/ramsey", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" } ], - "time": "2020-08-18T17:17:46+00:00" + "time": "2024-04-27T21:32:50+00:00" }, { "name": "react/promise", - "version": "v2.8.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4" + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4", - "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4505,7 +3973,23 @@ "authors": [ { "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com" + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], "description": "A lightweight implementation of CommonJS Promises/A for PHP", @@ -4515,147 +3999,40 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.8.0" - }, - "time": "2020-05-12T15:16:56+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "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/1.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "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/2.0.3" + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { - "name": "sebastian/comparator", - "version": "4.0.6", + "name": "sebastian/cli-parser", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -4670,31 +4047,16 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "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": [ { @@ -4702,33 +4064,32 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { - "name": "sebastian/diff", - "version": "4.0.4", + "name": "sebastian/code-unit", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -4743,24 +4104,15 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.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/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" }, "funding": [ { @@ -4768,35 +4120,32 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2023-02-03T06:58:43+00:00" }, { - "name": "sebastian/environment", - "version": "5.1.3", + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-posix": "*" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -4814,16 +4163,11 @@ "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/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" }, "funding": [ { @@ -4831,34 +4175,36 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2023-02-03T06:59:15+00:00" }, { - "name": "sebastian/exporter", - "version": "4.0.3", + "name": "sebastian/comparator", + "version": "5.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -4883,24 +4229,22 @@ "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/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + "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": [ { @@ -4908,38 +4252,33 @@ "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2024-10-18T14:56:07+00:00" }, { - "name": "sebastian/global-state", - "version": "4.0.0", + "name": "sebastian/complexity", + "version": "3.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bdb1e7c79e592b8c82cb1699be3c8743119b8a72" + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bdb1e7c79e592b8c82cb1699be3c8743119b8a72", - "reference": "bdb1e7c79e592b8c82cb1699be3c8743119b8a72", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { - "php": "^7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.0" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "3.2-dev" } }, "autoload": { @@ -4954,46 +4293,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/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/master" + "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" }, - "time": "2020-02-07T06:11:37+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "4.0.4", + "name": "sebastian/diff", + "version": "5.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -5009,13 +4352,24 @@ { "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/", + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "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": [ { @@ -5023,32 +4377,35 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { - "name": "sebastian/object-reflector", - "version": "2.0.4", + "name": "sebastian/environment", + "version": "6.1.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -5066,11 +4423,17 @@ "email": "sebastian@phpunit.de" } ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "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": [ { @@ -5078,32 +4441,34 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { - "name": "sebastian/recursion-context", - "version": "4.0.4", + "name": "sebastian/exporter", + "version": "5.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "" }, "require": { - "php": ">=7.3" + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -5124,16 +4489,29 @@ "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, + { + "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", + "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/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "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": [ { @@ -5141,32 +4519,35 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2024-03-02T07:17:12+00:00" }, { - "name": "sebastian/resource-operations", - "version": "3.0.3", + "name": "sebastian/global-state", + "version": "6.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "ext-dom": "*", + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -5184,11 +4565,15 @@ "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", + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "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": [ { @@ -5196,32 +4581,33 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { - "name": "sebastian/type", - "version": "2.3.1", + "name": "sebastian/lines-of-code", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { - "php": ">=7.3" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -5240,11 +4626,12 @@ "role": "lead" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "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/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + "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": [ { @@ -5252,29 +4639,34 @@ "type": "github" } ], - "time": "2020-10-26T13:18:59+00:00" + "time": "2023-12-21T08:38:20+00:00" }, { - "name": "sebastian/version", - "version": "3.0.2", + "name": "sebastian/object-enumerator", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -5289,15 +4681,14 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "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/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" }, "funding": [ { @@ -5305,308 +4696,264 @@ "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2023-02-03T07:08:32+00:00" }, { - "name": "seld/jsonlint", - "version": "1.8.3", + "name": "sebastian/object-reflector", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57", - "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0 || ^8.0" + "php": ">=8.1" }, "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": "3.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" - ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" }, "funding": [ { - "url": "https://github.com/Seldaek", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", - "type": "tidelift" } ], - "time": "2020-11-11T09:19:24+00:00" + "time": "2023-02-03T07:06:18+00:00" }, { - "name": "seld/phar-utils", - "version": "1.1.1", + "name": "sebastian/recursion-context", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8674b1d84ffb47cc59a101f5d5a3b61e87d23796", - "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", "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": "5.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" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "PHAR file format utilities, for when PHP phars you up", - "keywords": [ - "phar" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/Seldaek/phar-utils/issues", - "source": "https://github.com/Seldaek/phar-utils/tree/master" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" }, - "time": "2020-07-07T18:42:57+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" }, { - "name": "spomky-labs/otphp", - "version": "v10.0.1", + "name": "sebastian/type", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/Spomky-Labs/otphp.git", - "reference": "f44cce5a9db4b8da410215d992110482c931232f" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/f44cce5a9db4b8da410215d992110482c931232f", - "reference": "f44cce5a9db4b8da410215d992110482c931232f", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { - "beberlei/assert": "^3.0", - "ext-mbstring": "*", - "paragonie/constant_time_encoding": "^2.0", - "php": "^7.2|^8.0", - "thecodingmachine/safe": "^0.1.14|^1.0" + "php": ">=8.1" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^8.0", - "thecodingmachine/phpstan-safe-rule": "^1.0" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "v10.0": "10.0.x-dev", - "v9.0": "9.0.x-dev", - "v8.3": "8.3.x-dev" + "dev-main": "4.0-dev" } }, "autoload": { - "psr-4": { - "OTPHP\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/Spomky-Labs/otphp/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "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" - ], + "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/Spomky-Labs/otphp/issues", - "source": "https://github.com/Spomky-Labs/otphp/tree/v10.0.1" + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" }, - "time": "2020-01-28T09:24:19+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" }, { - "name": "symfony/console", - "version": "v4.4.22", + "name": "sebastian/version", + "version": "4.0.1", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "36bbd079b69b94bcc9c9c9e1e37ca3b1e7971625" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/36bbd079b69b94bcc9c9c9e1e37ca3b1e7971625", - "reference": "36bbd079b69b94bcc9c9c9e1e37ca3b1e7971625", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", - "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" - }, - "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": "" + "php": ">=8.1" }, "type": "library", + "extra": { + "branch-alias": { + "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": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "source": "https://github.com/symfony/console/tree/v4.4.22" + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2021-04-16T17:32:19+00:00" + "time": "2023-02-07T11:34:05+00:00" }, { - "name": "symfony/css-selector", - "version": "v5.2.7", + "name": "seld/jsonlint", + "version": "1.11.0", "source": { "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "59a684f5ac454f066ecbe6daecce6719aed283fb" + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/59a684f5ac454f066ecbe6daecce6719aed283fb", - "reference": "59a684f5ac454f066ecbe6daecce6719aed283fb", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": "^5.3 || ^7.0 || ^8.0" }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5614,70 +4961,61 @@ ], "authors": [ { - "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" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" } ], - "description": "Converts CSS selectors to XPath expressions", - "homepage": "https://symfony.com", + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.3.0-BETA1" + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/Seldaek", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", "type": "tidelift" } ], - "time": "2021-04-07T16:07:52+00:00" + "time": "2024-07-11T14:55:45+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v2.4.0", + "name": "seld/phar-utils", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=5.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "dev-master": "1.x-dev" } }, "autoload": { - "files": [ - "function.php" - ] + "psr-4": { + "Seld\\PharUtils\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5685,82 +5023,55 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" } ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.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": "2021-03-23T23:28:01+00:00" + "time": "2022-08-31T10:31:18+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v4.4.20", + "name": "seld/signal-handler", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c" + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c352647244bd376bf7d31efbd5401f13f50dad0c", - "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", "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" + "php": ">=7.2.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/error-handler": "~3.4|~4.4", - "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": "" + "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-main": "2.x-dev" + } + }, "autoload": { "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Seld\\Signal\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5768,69 +5079,64 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", + "keywords": [ + "posix", + "sigint", + "signal", + "sigterm", + "unix" + ], "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.20" + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.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": "2021-01-27T09:09:26+00:00" + "time": "2023-09-03T09:24:00+00:00" }, { - "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.9", + "name": "spomky-labs/otphp", + "version": "11.3.0", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7" + "url": "https://github.com/Spomky-Labs/otphp.git", + "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/84e23fdcd2517bf37aecbd16967e83f0caee25a7", - "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", + "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", "shasum": "" }, "require": { - "php": ">=7.1.3" + "ext-mbstring": "*", + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" }, - "suggest": { - "psr/event-dispatcher": "", - "symfony/event-dispatcher-implementation": "" + "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", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, "autoload": { "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" + "OTPHP\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -5839,65 +5145,89 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.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": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", + "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": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "FreeOTP", + "RFC 4226", + "RFC 6238", + "google authenticator", + "hotp", + "otp", + "totp" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.9" + "issues": "https://github.com/Spomky-Labs/otphp/issues", + "source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/Spomky", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" } ], - "time": "2020-07-06T13:19:58+00:00" + "time": "2024-06-12T11:22:32+00:00" }, { - "name": "symfony/filesystem", - "version": "v5.2.7", + "name": "symfony/console", + "version": "v6.4.20", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "056e92acc21d977c37e6ea8e97374b2a6c8551b0" + "url": "https://github.com/symfony/console.git", + "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/056e92acc21d977c37e6ea8e97374b2a6c8551b0", - "reference": "056e92acc21d977c37e6ea8e97374b2a6c8551b0", + "url": "https://api.github.com/repos/symfony/console/zipball/2e4af9c952617cc3f9559ff706aee420a8464c36", + "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8" + "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" + }, + "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\\Filesystem\\": "" + "Symfony\\Component\\Console\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5917,10 +5247,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides basic utilities for the filesystem", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.7" + "source": "https://github.com/symfony/console/tree/v6.4.20" }, "funding": [ { @@ -5936,29 +5272,29 @@ "type": "tidelift" } ], - "time": "2021-04-01T10:42:13+00:00" + "time": "2025-03-03T17:16:38+00:00" }, { - "name": "symfony/finder", - "version": "v5.2.4", + "name": "symfony/css-selector", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "0d639a0943822626290d169965804f79400e6a04" + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", - "reference": "0d639a0943822626290d169965804f79400e6a04", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": ">=8.2" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Finder\\": "" + "Symfony\\Component\\CssSelector\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -5973,15 +5309,19 @@ "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": "Finds files and directories via an intuitive fluent interface", + "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.4" + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" }, "funding": [ { @@ -5997,44 +5337,38 @@ "type": "tidelift" } ], - "time": "2021-02-15T18:55:04+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { - "name": "symfony/http-foundation", - "version": "v5.2.7", + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "a416487a73bb9c9d120e9ba3a60547f4a3fb7a1f" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a416487a73bb9c9d120e9ba3a60547f4a3fb7a1f", - "reference": "a416487a73bb9c9d120e9ba3a60547f4a3fb7a1f", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.15" - }, - "require-dev": { - "predis/predis": "~1.0", - "symfony/cache": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/mime": "^4.4|^5.0" - }, - "suggest": { - "symfony/mime": "To use the file extension guesser" + "php": ">=8.1" }, "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" }, - "exclude-from-classmap": [ - "/Tests/" + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6043,18 +5377,18 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Defines an object-oriented layer for the HTTP specification", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.2.7" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -6070,47 +5404,37 @@ "type": "tidelift" } ], - "time": "2021-05-01T13:46:24+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/mime", - "version": "v5.2.7", + "name": "symfony/dotenv", + "version": "v6.4.16", "source": { "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "7af452bf51c46f18da00feb32e1ad36db9426515" + "url": "https://github.com/symfony/dotenv.git", + "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/7af452bf51c46f18da00feb32e1ad36db9426515", - "reference": "7af452bf51c46f18da00feb32e1ad36db9426515", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/1ac5e7e7e862d4d574258daf08bd569ba926e4a5", + "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.1" }, "conflict": { - "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<4.4" + "symfony/console": "<5.4", + "symfony/process": "<5.4" }, "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/property-access": "^4.4|^5.1", - "symfony/property-info": "^4.4|^5.1", - "symfony/serializer": "^5.2" + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Mime\\": "" + "Symfony\\Component\\Dotenv\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -6130,14 +5454,15 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Allows manipulating MIME messages", + "description": "Registers environment variables from a .env file", "homepage": "https://symfony.com", - "keywords": [ - "mime", - "mime-type" + "keywords": [ + "dotenv", + "env", + "environment" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.2.7" + "source": "https://github.com/symfony/dotenv/tree/v6.4.16" }, "funding": [ { @@ -6153,44 +5478,51 @@ "type": "tidelift" } ], - "time": "2021-04-29T20:47:09+00:00" + "time": "2024-11-27T11:08:19+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.22.1", + "name": "symfony/event-dispatcher", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, - "suggest": { - "ext-ctype": "For best performance" + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } + "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\\Ctype\\": "" + "Symfony\\Component\\EventDispatcher\\": "" }, - "files": [ - "bootstrap.php" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6199,24 +5531,18 @@ ], "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": "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", - "ctype", - "polyfill", - "portable" - ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" }, "funding": [ { @@ -6232,47 +5558,40 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.22.1", + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "2d63434d922daf7da8dd863e7907e67ee3031483" + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/2d63434d922daf7da8dd863e7907e67ee3031483", - "reference": "2d63434d922daf7da8dd863e7907e67ee3031483", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" - }, - "suggest": { - "ext-intl": "For best performance" + "php": ">=8.1", + "psr/event-dispatcher": "^1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - }, - "files": [ - "bootstrap.php" - ] + "Symfony\\Contracts\\EventDispatcher\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6280,30 +5599,26 @@ ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Generic abstractions related to dispatching event", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, "funding": [ { @@ -6319,47 +5634,37 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.22.1", + "name": "symfony/filesystem", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", - "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "symfony/process": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + "Symfony\\Component\\Filesystem\\": "" }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6368,26 +5673,18 @@ ], "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 for intl's Normalizer class and related functions", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" }, "funding": [ { @@ -6403,44 +5700,35 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2024-10-25T15:15:23+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.23.0", + "name": "symfony/finder", + "version": "v6.4.17", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1" + "url": "https://github.com/symfony/finder.git", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1", - "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1", + "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, - "suggest": { - "ext-mbstring": "For best performance" + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Component\\Finder\\": "" }, - "files": [ - "bootstrap.php" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6449,25 +5737,18 @@ ], "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 for the Mbstring extension", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.0" + "source": "https://github.com/symfony/finder/tree/v6.4.17" }, "funding": [ { @@ -6483,41 +5764,52 @@ "type": "tidelift" } ], - "time": "2021-05-27T09:27:20+00:00" + "time": "2024-12-29T13:51:37+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.22.1", + "name": "symfony/mime", + "version": "v6.4.19", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9" + "url": "https://github.com/symfony/mime.git", + "reference": "ac537b6c55ccc2c749f3c979edfa9ec14aaed4f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", - "reference": "cc6e6f9b39fe8075b3dabfbaf5b5f645ae1340c9", + "url": "https://api.github.com/repos/symfony/mime/zipball/ac537b6c55ccc2c749f3c979edfa9ec14aaed4f3", + "reference": "ac537b6c55ccc2c749f3c979edfa9ec14aaed4f3", "shasum": "" }, "require": { - "php": ">=7.1" + "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-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } + "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\\Polyfill\\Php72\\": "" + "Symfony\\Component\\Mime\\": "" }, - "files": [ - "bootstrap.php" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -6526,24 +5818,22 @@ ], "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": "Allows manipulating MIME messages", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "mime", + "mime-type" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.1" + "source": "https://github.com/symfony/mime/tree/v6.4.19" }, "funding": [ { @@ -6559,45 +5849,45 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2025-02-17T21:23:52+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.22.1", + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], - "classmap": [ - "Resources/stubs" - ] + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6605,24 +5895,24 @@ ], "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": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "ctype", "polyfill", - "portable", - "shim" + "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -6638,55 +5928,48 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.22.1", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], - "classmap": [ - "Resources/stubs" - ] + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -6696,16 +5979,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "grapheme", + "intl", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -6721,33 +6006,43 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/process", - "version": "v4.4.25", + "name": "symfony/polyfill-intl-idn", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "cd61e6dd273975c6625316de9d141ebd197f93c9" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cd61e6dd273975c6625316de9d141ebd197f93c9", - "reference": "cd61e6dd273975c6625316de9d141ebd197f93c9", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", "shasum": "" }, "require": { - "php": ">=7.1.3" + "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": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6755,18 +6050,30 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Executes commands in sub-processes", + "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/process/tree/v4.4.25" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" }, "funding": [ { @@ -6782,43 +6089,45 @@ "type": "tidelift" } ], - "time": "2021-05-26T11:20:16+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/service-contracts", - "version": "v2.4.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1" + "php": ">=7.2" }, "suggest": { - "symfony/service-implementation": "" + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6834,18 +6143,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -6861,43 +6170,45 @@ "type": "tidelift" } ], - "time": "2021-04-01T10:43:52+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/yaml", - "version": "v4.4.22", + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "1c2fd24147961525eaefb65b11987cab75adab59" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/1c2fd24147961525eaefb65b11987cab75adab59", - "reference": "1c2fd24147961525eaefb65b11987cab75adab59", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" + "php": ">=7.2" }, - "require-dev": { - "symfony/console": "^3.4|^4.0|^5.0" + "provide": { + "ext-mbstring": "*" }, "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "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\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6905,18 +6216,25 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Loads and dumps YAML files", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], "support": { - "source": "https://github.com/symfony/yaml/tree/v4.4.22" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -6932,308 +6250,198 @@ "type": "tidelift" } ], - "time": "2021-04-23T12:09:37+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "thecodingmachine/safe", - "version": "v1.3.3", + "name": "symfony/polyfill-php73", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { "php": ">=7.2" }, - "require-dev": { - "phpstan/phpstan": "^0.12", - "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^0.12" - }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "0.1-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Safe\\": [ - "lib/", - "deprecated/", - "generated/" - ] + "Symfony\\Polyfill\\Php73\\": "" }, - "files": [ - "deprecated/apc.php", - "deprecated/libevent.php", - "deprecated/mssql.php", - "deprecated/stats.php", - "lib/special_cases.php", - "generated/apache.php", - "generated/apcu.php", - "generated/array.php", - "generated/bzip2.php", - "generated/calendar.php", - "generated/classobj.php", - "generated/com.php", - "generated/cubrid.php", - "generated/curl.php", - "generated/datetime.php", - "generated/dir.php", - "generated/eio.php", - "generated/errorfunc.php", - "generated/exec.php", - "generated/fileinfo.php", - "generated/filesystem.php", - "generated/filter.php", - "generated/fpm.php", - "generated/ftp.php", - "generated/funchand.php", - "generated/gmp.php", - "generated/gnupg.php", - "generated/hash.php", - "generated/ibase.php", - "generated/ibmDb2.php", - "generated/iconv.php", - "generated/image.php", - "generated/imap.php", - "generated/info.php", - "generated/ingres-ii.php", - "generated/inotify.php", - "generated/json.php", - "generated/ldap.php", - "generated/libxml.php", - "generated/lzf.php", - "generated/mailparse.php", - "generated/mbstring.php", - "generated/misc.php", - "generated/msql.php", - "generated/mysql.php", - "generated/mysqli.php", - "generated/mysqlndMs.php", - "generated/mysqlndQc.php", - "generated/network.php", - "generated/oci8.php", - "generated/opcache.php", - "generated/openssl.php", - "generated/outcontrol.php", - "generated/password.php", - "generated/pcntl.php", - "generated/pcre.php", - "generated/pdf.php", - "generated/pgsql.php", - "generated/posix.php", - "generated/ps.php", - "generated/pspell.php", - "generated/readline.php", - "generated/rpminfo.php", - "generated/rrd.php", - "generated/sem.php", - "generated/session.php", - "generated/shmop.php", - "generated/simplexml.php", - "generated/sockets.php", - "generated/sodium.php", - "generated/solr.php", - "generated/spl.php", - "generated/sqlsrv.php", - "generated/ssdeep.php", - "generated/ssh2.php", - "generated/stream.php", - "generated/strings.php", - "generated/swoole.php", - "generated/uodbc.php", - "generated/uopz.php", - "generated/url.php", - "generated/var.php", - "generated/xdiff.php", - "generated/xml.php", - "generated/xmlrpc.php", - "generated/yaml.php", - "generated/yaz.php", - "generated/zip.php", - "generated/zlib.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "support": { - "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" - }, - "time": "2020-10-28T17:51:34+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" }, "funding": [ { - "url": "https://github.com/theseer", + "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-07-12T23:59:07+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "vlucas/phpdotenv", - "version": "v2.6.7", + "name": "symfony/polyfill-php80", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "b786088918a884258c9e3e27405c6a4cf2ee246e" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/b786088918a884258c9e3e27405c6a4cf2ee246e", - "reference": "b786088918a884258c9e3e27405c6a4cf2ee246e", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": "^5.3.9 || ^7.0 || ^8.0", - "symfony/polyfill-ctype": "^1.17" - }, - "require-dev": { - "ext-filter": "*", - "ext-pcre": "*", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20" - }, - "suggest": { - "ext-filter": "Required to use the boolean validator.", - "ext-pcre": "Required to use most of the library." + "php": ">=7.2" }, "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\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Graham Campbell", - "email": "graham@alt-three.com", - "homepage": "https://gjcampbell.co.uk/" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Vance Lucas", - "email": "vance@vancelucas.com", - "homepage": "https://vancelucas.com/" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "dotenv", - "env", - "environment" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v2.6.7" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { - "url": "https://github.com/GrahamCampbell", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2021-01-20T14:39:13+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "webmozart/assert", - "version": "1.10.0", + "name": "symfony/polyfill-php81", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.10-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Webmozart\\Assert\\": "src/" - } + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7241,44 +6449,65 @@ ], "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 backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "assert", - "check", - "validate" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" }, - "time": "2021-03-09T10:59:23+00:00" + "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": "weew/helpers-array", - "version": "v1.3.1", + "name": "symfony/process", + "version": "v6.4.20", "source": { "type": "git", - "url": "https://github.com/weew/helpers-array.git", - "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0" + "url": "https://github.com/symfony/process.git", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/weew/helpers-array/zipball/9bff63111f9765b4277750db8d276d92b3e16ed0", - "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0", + "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", "shasum": "" }, - "require-dev": { - "phpunit/phpunit": "^4.7", - "satooshi/php-coveralls": "^0.6.1" + "require": { + "php": ">=8.1" }, "type": "library", "autoload": { - "files": [ - "src/array.php" + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7287,68 +6516,73 @@ ], "authors": [ { - "name": "Maxim Kott", - "email": "maximkott@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Useful collection of php array helpers.", + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/weew/helpers-array/issues", - "source": "https://github.com/weew/helpers-array/tree/master" + "source": "https://github.com/symfony/process/tree/v6.4.20" }, - "time": "2016-07-21T11:18:01+00:00" - } - ], - "packages-dev": [ + "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-03-10T17:11:00+00:00" + }, { - "name": "brainmaestro/composer-git-hooks", - "version": "v2.8.5", + "name": "symfony/service-contracts", + "version": "v3.5.1", "source": { "type": "git", - "url": "https://github.com/BrainMaestro/composer-git-hooks.git", - "reference": "ffed8803690ac12214082120eee3441b00aa390e" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/ffed8803690ac12214082120eee3441b00aa390e", - "reference": "ffed8803690ac12214082120eee3441b00aa390e", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { - "php": "^5.6 || >=7.0", - "symfony/console": "^3.2 || ^4.0 || ^5.0" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, - "require-dev": { - "ext-json": "*", - "friendsofphp/php-cs-fixer": "^2.9", - "phpunit/phpunit": "^5.7 || ^7.0" + "conflict": { + "ext-psr": "<1.1|>=2" }, - "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/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { - "BrainMaestro\\GitHooks\\": "src/" + "Symfony\\Contracts\\Service\\": "" }, - "files": [ - "src/helpers.php" + "exclude-from-classmap": [ + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7357,52 +6591,85 @@ ], "authors": [ { - "name": "Ezinwa Okpoechi", - "email": "brainmaestro@outlook.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": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", "keywords": [ - "HOOK", - "composer", - "git" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "issues": "https://github.com/BrainMaestro/composer-git-hooks/issues", - "source": "https://github.com/BrainMaestro/composer-git-hooks/tree/v2.8.5" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, - "time": "2021-02-08T15:59:11+00:00" + "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:20:29+00:00" }, { - "name": "codacy/coverage", - "version": "1.4.3", + "name": "symfony/string", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/codacy/php-codacy-coverage.git", - "reference": "1852ca987c91ef466ebcfdbdd4e1788b653eaf1d" + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codacy/php-codacy-coverage/zipball/1852ca987c91ef466ebcfdbdd4e1788b653eaf1d", - "reference": "1852ca987c91ef466ebcfdbdd4e1788b653eaf1d", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", "shasum": "" }, "require": { - "gitonomy/gitlib": ">=1.0", - "php": ">=5.3.3", - "symfony/console": "~2.5|~3.0|~4.0|~5.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" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "clue/phar-composer": "^1.1", - "phpunit/phpunit": "~6.5" + "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/codacycoverage" - ], "type": "library", "autoload": { - "classmap": [ - "src/" + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7411,106 +6678,86 @@ ], "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": "Sends PHP test coverage information to Codacy.", - "homepage": "https://github.com/codacy/php-codacy-coverage", + "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": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], "support": { - "issues": "https://github.com/codacy/php-codacy-coverage/issues", - "source": "https://github.com/codacy/php-codacy-coverage/tree/master" - }, - "abandoned": true, - "time": "2020-01-10T10:52:12+00:00" - }, - { - "name": "codeception/aspect-mock", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/AspectMock.git", - "reference": "eef5e5e9ebd66c89d6184416e83851c354963e9c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/AspectMock/zipball/eef5e5e9ebd66c89d6184416e83851c354963e9c", - "reference": "eef5e5e9ebd66c89d6184416e83851c354963e9c", - "shasum": "" - }, - "require": { - "goaop/framework": "^2.2.0", - "php": "^7.0", - "phpunit/phpunit": "> 6.0.0", - "symfony/finder": ">=2.4 <6.0" + "source": "https://github.com/symfony/string/tree/v7.2.0" }, - "require-dev": { - "codeception/codeception": "^4.0", - "codeception/specify": "^1.0", - "codeception/verify": "^1.2" - }, - "type": "library", - "autoload": { - "psr-0": { - "AspectMock": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Michael Bodnarchuk", - "email": "davert@codeception.com" + "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": "Experimental Mocking Framework powered by Aspects", - "support": { - "issues": "https://github.com/Codeception/AspectMock/issues", - "source": "https://github.com/Codeception/AspectMock/tree/3.1.1" - }, - "time": "2021-04-10T19:47:51+00:00" + "time": "2024-11-13T13:31:26+00:00" }, { - "name": "doctrine/cache", - "version": "1.11.3", + "name": "symfony/var-dumper", + "version": "v7.2.3", "source": { "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "3bb5588cec00a0268829cc4a518490df6741af9d" + "url": "https://github.com/symfony/var-dumper.git", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/3bb5588cec00a0268829cc4a518490df6741af9d", - "reference": "3bb5588cec00a0268829cc4a518490df6741af9d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", "shasum": "" }, "require": { - "php": "~7.1 || ^8.0" + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "doctrine/common": ">2.2,<2.4", - "psr/cache": ">=3" + "symfony/console": "<6.4" }, "require-dev": { - "alcaeus/mongo-php-adapter": "^1.1", - "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^8.0", - "mongodb/mongodb": "^1.1", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "predis/predis": "~1.0", - "psr/cache": "^1.0 || ^2.0", - "symfony/cache": "^4.4 || ^5.2" - }, - "suggest": { - "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + "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", "autoload": { + "files": [ + "Resources/functions/dump.php" + ], "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" - } + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7518,93 +6765,75 @@ ], "authors": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", - "homepage": "https://www.doctrine-project.org/projects/cache.html", + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", "keywords": [ - "abstraction", - "apcu", - "cache", - "caching", - "couchdb", - "memcached", - "php", - "redis", - "xcache" + "debug", + "dump" ], "support": { - "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/1.11.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", + "url": "https://symfony.com/sponsor", "type": "custom" }, { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" + "url": "https://github.com/fabpot", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2021-05-25T09:01:55+00:00" + "time": "2025-01-17T11:39:41+00:00" }, { - "name": "gitonomy/gitlib", - "version": "v1.2.3", + "name": "symfony/yaml", + "version": "v7.2.5", "source": { "type": "git", - "url": "https://github.com/gitonomy/gitlib.git", - "reference": "d22f212b97fdb631ac73dfae65c194dc4cb0d227" + "url": "https://github.com/symfony/yaml.git", + "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/gitonomy/gitlib/zipball/d22f212b97fdb631ac73dfae65c194dc4cb0d227", - "reference": "d22f212b97fdb631ac73dfae65c194dc4cb0d227", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", + "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", "shasum": "" }, "require": { - "ext-pcre": "*", - "php": "^5.6 || ^7.0 || ^8.0", - "symfony/polyfill-mbstring": "^1.7", - "symfony/process": "^3.4 || ^4.0 || ^5.0" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" }, - "require-dev": { - "ext-fileinfo": "*", - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0", - "psr/log": "^1.0" + "conflict": { + "symfony/console": "<6.4" }, - "suggest": { - "ext-fileinfo": "Required to determine the mimetype of a blob", - "psr/log": "Required to use loggers for reporting of execution" + "require-dev": { + "symfony/console": "^6.4|^7.0" }, + "bin": [ + "Resources/bin/yaml-lint" + ], "type": "library", "autoload": { "psr-4": { - "Gitonomy\\Git\\": "src/Gitonomy/Git/" - } + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7612,149 +6841,107 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "graham@alt-three.com" - }, - { - "name": "Julien Didier", - "email": "genzo.wm@gmail.com" - }, - { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Alexandre Salomé", - "email": "alexandre.salome@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library for accessing git", + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/gitonomy/gitlib/issues", - "source": "https://github.com/gitonomy/gitlib/tree/v1.2.3" + "source": "https://github.com/symfony/yaml/tree/v7.2.5" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/gitonomy/gitlib", + "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-12-29T16:48:45+00:00" + "time": "2025-03-03T07:12:39+00:00" }, { - "name": "goaop/framework", - "version": "2.3.5", + "name": "theseer/tokenizer", + "version": "1.2.3", "source": { "type": "git", - "url": "https://github.com/goaop/framework.git", - "reference": "19a6f2110c71aed99081ca23c359436b723a6cdf" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/goaop/framework/zipball/19a6f2110c71aed99081ca23c359436b723a6cdf", - "reference": "19a6f2110c71aed99081ca23c359436b723a6cdf", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { - "doctrine/annotations": "^1.2.3", - "doctrine/cache": "^1.5", - "goaop/parser-reflection": "~2.0", - "jakubledl/dissect": "~1.0", - "php": "~7.0", - "symfony/finder": "^3.4|^4.2|^5.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", - "webmozart/glob": "^4.1" - }, - "suggest": { - "symfony/console": "Enables the usage of the command-line tool." + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, - "bin": [ - "bin/aspect" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, "autoload": { - "psr-4": { - "Go\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Lisachenko Alexander", - "homepage": "https://github.com/lisachenko" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "Framework for aspect-oriented programming in PHP.", - "homepage": "http://go.aopphp.com/", - "keywords": [ - "aop", - "aspect", - "library", - "php" - ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "issues": "https://github.com/goaop/framework/issues", - "source": "https://github.com/goaop/framework/tree/2.3.5" + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { - "url": "https://github.com/lisachenko", + "url": "https://github.com/theseer", "type": "github" } ], - "time": "2021-06-11T16:17:30+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { - "name": "goaop/parser-reflection", - "version": "2.1.3", + "name": "weew/helpers-array", + "version": "v1.3.1", "source": { "type": "git", - "url": "https://github.com/goaop/parser-reflection.git", - "reference": "2e837e150e15d38f7004b0dbcd0af4abe034c9e2" + "url": "https://github.com/weew/helpers-array.git", + "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/goaop/parser-reflection/zipball/2e837e150e15d38f7004b0dbcd0af4abe034c9e2", - "reference": "2e837e150e15d38f7004b0dbcd0af4abe034c9e2", + "url": "https://api.github.com/repos/weew/helpers-array/zipball/9bff63111f9765b4277750db8d276d92b3e16ed0", + "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0", "shasum": "" }, - "require": { - "nikic/php-parser": "^4.0 <4.7.0", - "php": ">=7.1" - }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "^4.7", + "satooshi/php-coveralls": "^0.6.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "autoload": { - "psr-4": { - "Go\\ParserReflection\\": "src" - }, "files": [ - "src/bootstrap.php" - ], - "exclude-from-classmap": [ - "/tests/" + "src/array.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -7763,100 +6950,116 @@ ], "authors": [ { - "name": "Alexander Lisachenko", - "email": "lisachenko.it@gmail.com" + "name": "Maxim Kott", + "email": "maximkott@gmail.com" } ], - "description": "Provides reflection information, based on raw source", + "description": "Useful collection of php array helpers.", "support": { - "issues": "https://github.com/goaop/parser-reflection/issues", - "source": "https://github.com/goaop/parser-reflection/tree/2.x" + "issues": "https://github.com/weew/helpers-array/issues", + "source": "https://github.com/weew/helpers-array/tree/master" }, - "time": "2020-08-13T21:02:42+00:00" - }, + "time": "2016-07-21T11:18:01+00:00" + } + ], + "packages-dev": [ { - "name": "jakubledl/dissect", - "version": "v1.0.1", + "name": "brainmaestro/composer-git-hooks", + "version": "v3.0.0", "source": { "type": "git", - "url": "https://github.com/jakubledl/dissect.git", - "reference": "d3a391de31e45a247e95cef6cf58a91c05af67c4" + "url": "https://github.com/BrainMaestro/composer-git-hooks.git", + "reference": "684dc85f480268baf5e13f39a3cc494eeb2536e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jakubledl/dissect/zipball/d3a391de31e45a247e95cef6cf58a91c05af67c4", - "reference": "d3a391de31e45a247e95cef6cf58a91c05af67c4", + "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/684dc85f480268baf5e13f39a3cc494eeb2536e8", + "reference": "684dc85f480268baf5e13f39a3cc494eeb2536e8", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^8.0", + "symfony/console": "^5.0|^6.0|^7.0" }, "require-dev": { - "symfony/console": "~2.1" - }, - "suggest": { - "symfony/console": "for the command-line tool" + "ext-json": "*", + "friendsofphp/php-cs-fixer": "^3.0", + "phpunit/phpunit": "^9|^10|^11" }, "bin": [ - "bin/dissect.php", - "bin/dissect" + "cghooks" ], "type": "library", + "extra": { + "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": { - "psr-0": { - "Dissect": [ - "src/" - ] + "files": [ + "src/helpers.php" + ], + "psr-4": { + "BrainMaestro\\GitHooks\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "unlicense" + "MIT" ], "authors": [ { - "name": "Jakub Lédl", - "email": "jakubledl@gmail.com" + "name": "Ezinwa Okpoechi", + "email": "brainmaestro@outlook.com" } ], - "description": "Lexing and parsing in pure PHP", - "homepage": "https://github.com/jakubledl/dissect", + "description": "Easily manage git hooks in your composer config", "keywords": [ - "ast", - "lexing", - "parser", - "parsing" + "HOOK", + "composer", + "git" ], "support": { - "issues": "https://github.com/jakubledl/dissect/issues", - "source": "https://github.com/jakubledl/dissect/tree/v1.0.1" + "issues": "https://github.com/BrainMaestro/composer-git-hooks/issues", + "source": "https://github.com/BrainMaestro/composer-git-hooks/tree/v3.0.0" }, - "time": "2013-01-29T21:29:14+00:00" + "time": "2024-06-22T09:17:59+00:00" }, { "name": "pdepend/pdepend", - "version": "2.9.1", + "version": "2.16.2", "source": { "type": "git", "url": "https://github.com/pdepend/pdepend.git", - "reference": "1632f0cee84512ffd6dde71e58536b3b06528c41" + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdepend/pdepend/zipball/1632f0cee84512ffd6dde71e58536b3b06528c41", - "reference": "1632f0cee84512ffd6dde71e58536b3b06528c41", + "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", "gregwar/rst": "^1.0", - "phpunit/phpunit": "^4.8.36|^5.7.27", "squizlabs/php_codesniffer": "^2.0.0" }, "bin": [ @@ -7878,9 +7081,15 @@ "BSD-3-Clause" ], "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], "support": { "issues": "https://github.com/pdepend/pdepend/issues", - "source": "https://github.com/pdepend/pdepend/tree/2.9.1" + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" }, "funding": [ { @@ -7888,35 +7097,35 @@ "type": "tidelift" } ], - "time": "2021-04-15T21:36:28+00:00" + "time": "2023-12-17T18:09:59+00:00" }, { "name": "php-coveralls/php-coveralls", - "version": "v2.4.3", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/php-coveralls/php-coveralls.git", - "reference": "909381bd40a17ae6e9076051f0d73293c1c091af" + "reference": "b36fa4394e519dafaddc04ae03976bc65a25ba15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/909381bd40a17ae6e9076051f0d73293c1c091af", - "reference": "909381bd40a17ae6e9076051f0d73293c1c091af", + "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/b36fa4394e519dafaddc04ae03976bc65a25ba15", + "reference": "b36fa4394e519dafaddc04ae03976bc65a25ba15", "shasum": "" }, "require": { "ext-json": "*", "ext-simplexml": "*", "guzzlehttp/guzzle": "^6.0 || ^7.0", - "php": "^5.5 || ^7.0 || ^8.0", - "psr/log": "^1.0", - "symfony/config": "^2.1 || ^3.0 || ^4.0 || ^5.0", - "symfony/console": "^2.1 || ^3.0 || ^4.0 || ^5.0", - "symfony/stopwatch": "^2.0 || ^3.0 || ^4.0 || ^5.0", - "symfony/yaml": "^2.0.5 || ^3.0 || ^4.0 || ^5.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 || ^7.0 || ^8.0 || ^9.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": { @@ -7969,28 +7178,28 @@ ], "support": { "issues": "https://github.com/php-coveralls/php-coveralls/issues", - "source": "https://github.com/php-coveralls/php-coveralls/tree/v2.4.3" + "source": "https://github.com/php-coveralls/php-coveralls/tree/v2.7.0" }, - "time": "2020-12-24T09:17:03+00:00" + "time": "2023-11-22T10:21:01+00:00" }, { "name": "phpmd/phpmd", - "version": "2.10.0", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/phpmd/phpmd.git", - "reference": "58ef9e746a1ab50ad3360d5d301e1229ed2612cb" + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmd/phpmd/zipball/58ef9e746a1ab50ad3360d5d301e1229ed2612cb", - "reference": "58ef9e746a1ab50ad3360d5d301e1229ed2612cb", + "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.9.1", + "pdepend/pdepend": "^2.16.1", "php": ">=5.3.9" }, "require-dev": { @@ -7999,8 +7208,7 @@ "ext-simplexml": "*", "gregwar/rst": "^1.0", "mikey179/vfsstream": "^1.6.8", - "phpunit/phpunit": "^4.8.36 || ^5.7.27", - "squizlabs/php_codesniffer": "^2.0" + "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2" }, "bin": [ "src/bin/phpmd" @@ -8037,6 +7245,7 @@ "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", @@ -8046,7 +7255,7 @@ "support": { "irc": "irc://irc.freenode.org/phpmd", "issues": "https://github.com/phpmd/phpmd/issues", - "source": "https://github.com/phpmd/phpmd/tree/2.10.0" + "source": "https://github.com/phpmd/phpmd/tree/2.15.0" }, "funding": [ { @@ -8054,217 +7263,203 @@ "type": "tidelift" } ], - "time": "2021-04-26T18:44:44+00:00" + "time": "2023-12-11T08:22:20+00:00" }, { - "name": "sebastian/cli-parser", - "version": "1.0.1", + "name": "squizlabs/php_codesniffer", + "version": "3.10.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", + "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", "shasum": "" }, "require": { - "php": ">=7.3" + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "3.x-dev" } }, - "autoload": { - "classmap": [ - "src/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + "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/sebastianbergmann", + "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-09-28T06:08:49+00:00" + "time": "2024-09-18T10:38:58+00:00" }, { - "name": "sebastian/phpcpd", - "version": "6.0.3", + "name": "symfony/config", + "version": "v7.2.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpcpd.git", - "reference": "f3683aa0db2e8e09287c2bb33a595b2873ea9176" + "url": "https://github.com/symfony/config.git", + "reference": "7716594aaae91d9141be080240172a92ecca4d44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/f3683aa0db2e8e09287c2bb33a595b2873ea9176", - "reference": "f3683aa0db2e8e09287c2bb33a595b2873ea9176", + "url": "https://api.github.com/repos/symfony/config/zipball/7716594aaae91d9141be080240172a92ecca4d44", + "reference": "7716594aaae91d9141be080240172a92ecca4d44", "shasum": "" }, "require": { - "ext-dom": "*", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0", - "phpunit/php-timer": "^5.0", - "sebastian/cli-parser": "^1.0", - "sebastian/version": "^3.0" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1", + "symfony/polyfill-ctype": "~1.8" }, - "bin": [ - "phpcpd" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.0-dev" - } + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "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", "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Copy/Paste Detector (CPD) for PHP code.", - "homepage": "https://github.com/sebastianbergmann/phpcpd", + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/sebastianbergmann/phpcpd/issues", - "source": "https://github.com/sebastianbergmann/phpcpd/tree/6.0.3" + "source": "https://github.com/symfony/config/tree/v7.2.3" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" - } - ], - "time": "2020-12-07T05:39:23+00:00" - }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.5.8", - "source": { - "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" - }, - "bin": [ - "bin/phpcs", - "bin/phpcbf" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + }, { - "name": "Greg Sherwood", - "role": "lead" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "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", - "keywords": [ - "phpcs", - "standards" - ], - "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" - }, - "time": "2020-10-23T02:01:07+00:00" + "time": "2025-01-22T12:07:01+00:00" }, { - "name": "symfony/config", - "version": "v4.4.22", + "name": "symfony/dependency-injection", + "version": "v7.2.5", "source": { "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "f6d8318c14e4be81525ae47b30e618f0bed4c7b3" + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "58ab71379f14a741755717cece2868bf41ed45d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/f6d8318c14e4be81525ae47b30e618f0bed4c7b3", - "reference": "f6d8318c14e4be81525ae47b30e618f0bed4c7b3", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/58ab71379f14a741755717cece2868bf41ed45d8", + "reference": "58ab71379f14a741755717cece2868bf41ed45d8", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/filesystem": "^3.4|^4.0|^5.0", - "symfony/polyfill-ctype": "~1.8" + "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.20|^7.2.5" }, "conflict": { - "symfony/finder": "<3.4" + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" }, - "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" + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" + "require-dev": { + "symfony/config": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Config\\": "" + "Symfony\\Component\\DependencyInjection\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8284,10 +7479,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v4.4.22" + "source": "https://github.com/symfony/dependency-injection/tree/v7.2.5" }, "funding": [ { @@ -8303,53 +7498,30 @@ "type": "tidelift" } ], - "time": "2021-04-07T15:47:03+00:00" + "time": "2025-03-13T12:21:46+00:00" }, { - "name": "symfony/dependency-injection", - "version": "v4.4.22", + "name": "symfony/stopwatch", + "version": "v7.2.4", "source": { "type": "git", - "url": "https://github.com/symfony/dependency-injection.git", - "reference": "778b140b3e8f6890f43dc2c978e58e69f188909a" + "url": "https://github.com/symfony/stopwatch.git", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/778b140b3e8f6890f43dc2c978e58e69f188909a", - "reference": "778b140b3e8f6890f43dc2c978e58e69f188909a", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", "shasum": "" }, "require": { - "php": ">=7.1.3", - "psr/container": "^1.0", - "symfony/service-contracts": "^1.1.6|^2" - }, - "conflict": { - "symfony/config": "<4.3|>=5.0", - "symfony/finder": "<3.4", - "symfony/proxy-manager-bridge": "<3.4", - "symfony/yaml": "<3.4" - }, - "provide": { - "psr/container-implementation": "1.0", - "symfony/service-implementation": "1.0|2.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": "" + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\DependencyInjection\\": "" + "Symfony\\Component\\Stopwatch\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8369,10 +7541,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v4.4.22" + "source": "https://github.com/symfony/stopwatch/tree/v7.2.4" }, "funding": [ { @@ -8388,30 +7560,34 @@ "type": "tidelift" } ], - "time": "2021-04-07T15:47:03+00:00" + "time": "2025-02-24T10:49:57+00:00" }, { - "name": "symfony/stopwatch", - "version": "v5.3.0", + "name": "symfony/var-exporter", + "version": "v7.2.5", "source": { "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "313d02f59d6543311865007e5ff4ace05b35ee65" + "url": "https://github.com/symfony/var-exporter.git", + "reference": "c37b301818bd7288715d40de634f05781b686ace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/313d02f59d6543311865007e5ff4ace05b35ee65", - "reference": "313d02f59d6543311865007e5ff4ace05b35ee65", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/c37b301818bd7288715d40de634f05781b686ace", + "reference": "c37b301818bd7288715d40de634f05781b686ace", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/service-contracts": "^1.0|^2" + "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": { "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" + "Symfony\\Component\\VarExporter\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8423,18 +7599,28 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a way to profile code", + "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/stopwatch/tree/v5.3.0" + "source": "https://github.com/symfony/var-exporter/tree/v7.2.5" }, "funding": [ { @@ -8450,7 +7636,7 @@ "type": "tidelift" } ], - "time": "2021-05-26T17:43:10+00:00" + "time": "2025-03-13T12:21:46+00:00" } ], "aliases": [], @@ -8459,13 +7645,14 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.3", "ext-curl": "*", "ext-dom": "*", + "ext-iconv": "*", "ext-intl": "*", "ext-json": "*", - "ext-openssl": "*" + "ext-openssl": "*", + "php": ">=8.2" }, "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.6.0" } diff --git a/dev/tests/_bootstrap.php b/dev/tests/_bootstrap.php index 9d3750ec6..7da6cffa7 100644 --- a/dev/tests/_bootstrap.php +++ b/dev/tests/_bootstrap.php @@ -15,23 +15,6 @@ require_once $mftfTestCasePath; require_once $mftfStaticTestCasePath; -// 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' -]); - // set mftf appplication context \Magento\FunctionalTestingFramework\Config\MftfApplicationConfig::create( true, @@ -53,7 +36,7 @@ foreach ($TEST_ENVS as $key => $value) { $_ENV[$key] = $value; - putenv("{$key}=${value}"); + putenv("{$key}={$value}"); } // Add our test module to the allowlist diff --git a/dev/tests/functional/standalone_bootstrap.php b/dev/tests/functional/standalone_bootstrap.php index 5ce6f029a..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,19 +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->setEnvironmentVariable('VERBOSE_ARTIFACTS', VERBOSE_ARTIFACTS); + $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); diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Data/DeprecatedMessageData.xml b/dev/tests/functional/tests/MFTF/DevDocs/Data/DeprecatedMessageData.xml index ba1460a60..8be85b447 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Data/DeprecatedMessageData.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/Data/DeprecatedMessageData.xml @@ -9,7 +9,7 @@ - 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/MessageData.xml b/dev/tests/functional/tests/MFTF/DevDocs/Data/MessageData.xml index f35812e54..2a973ce53 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Data/MessageData.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/Data/MessageData.xml @@ -9,7 +9,7 @@ - Introduction to the Magento Functional Testing Framework + Introduction to the Functional Testing Framework 0 1 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 bda1cfb57..911bc65eb 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml @@ -42,6 +42,14 @@ 987
+ + some text + + + some text + getText + + @@ -51,5 +59,6 @@ {{ExtendedMessageData.numbers}} ["Something New", "0", "1", "2", "3", "TESTING CASE"] + diff --git a/dev/tests/phpunit.xml b/dev/tests/phpunit.xml index 1014ec7c3..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 8985f2407..5e539aeaf 100644 --- a/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php +++ b/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php @@ -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']; 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 b009af5ff..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. */ +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(): void + 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/Console/BaseGenerateCommandTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Console/BaseGenerateCommandTest.php index d2a58c0ff..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. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Console; -use AspectMock\Test as AspectMock; -use PHPUnit\Framework\TestCase; +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(): void + /** + * @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 index 40a96e774..3e00c34a5 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Console/GenerateTestsCommandTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Console/GenerateTestsCommandTest.php @@ -17,6 +17,7 @@ class GenerateTestsCommandTest extends TestCase * @param mixed $expected * @return void * @dataProvider configParallelOptions + * @throws \ReflectionException */ public function testParseConfigParallelOptions($time, $groups, $expected): void { @@ -40,7 +41,7 @@ public function testParseConfigParallelOptions($time, $groups, $expected): void * * @return array */ - public function configParallelOptions(): array + public static function configParallelOptions(): array { return [ [null, null, ['parallelByTime', 600000]], /* #0 */ 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 745789501..059a94057 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php @@ -3,17 +3,19 @@ * 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 ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use tests\unit\Util\ObjectHandlerUtil; use tests\unit\Util\TestLoggingUtil; /** @@ -22,9 +24,9 @@ class DataObjectHandlerTest extends MagentoTestCase { /** - * Setup method + * @inheritDoc */ - public function setUp(): void + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } @@ -145,11 +147,14 @@ public function setUp(): void ]; /** - * 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 { - ObjectHandlerUtil::mockDataObjectHandlerWithData(self::PARSER_OUTPUT); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT); // Call the method under test $actual = DataObjectHandler::getInstance()->getAllObjects(); @@ -161,29 +166,35 @@ public function testGetAllObjects() } /** - * test deprecated data object + * Validate test deprecated data object. + * + * @return void + * @throws Exception */ - public function testDeprecatedDataObject() + public function testDeprecatedDataObject(): void { - ObjectHandlerUtil::mockDataObjectHandlerWithData(self::PARSER_OUTPUT_DEPRECATED); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT_DEPRECATED); // Call the method under test - $actual = DataObjectHandler::getInstance()->getAllObjects(); + DataObjectHandler::getInstance()->getAllObjects(); //validate deprecation warning TestLoggingUtil::getInstance()->validateMockLogStatement( 'warning', - "DEPRECATION: The data entity 'EntityOne' is deprecated.", - ["fileName" => "filename.xml", "deprecatedMessage" => "deprecation message"] + 'DEPRECATION: The data entity \'EntityOne\' is deprecated.', + ['fileName' => 'filename.xml', 'deprecatedMessage' => 'deprecation message'] ); } /** - * getObject should return the expected data object if it exists + * Validate getObject should return the expected data object if it exists. + * + * @return void + * @throws Exception */ - public function testGetObject() + public function testGetObject(): void { - ObjectHandlerUtil::mockDataObjectHandlerWithData(self::PARSER_OUTPUT); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT); // Call the method under test $actual = DataObjectHandler::getInstance()->getObject('EntityOne'); @@ -194,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 { - ObjectHandlerUtil::mockDataObjectHandlerWithData(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 { - ObjectHandlerUtil::mockDataObjectHandlerWithData(self::PARSER_OUTPUT_WITH_EXTEND); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT_WITH_EXTEND); // Call the method under test $actual = DataObjectHandler::getInstance()->getAllObjects(); @@ -229,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 { - ObjectHandlerUtil::mockDataObjectHandlerWithData(self::PARSER_OUTPUT_WITH_EXTEND); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT_WITH_EXTEND); // Call the method under test $actual = DataObjectHandler::getInstance()->getObject('EntityTwo'); @@ -252,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 { - ObjectHandlerUtil::mockDataObjectHandlerWithData(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'] ); @@ -269,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 { - ObjectHandlerUtil::mockDataObjectHandlerWithData(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'] ); @@ -288,10 +314,65 @@ public function testGetObjectWithDataExtendsItself() } /** - * clean up function runs after all tests + * Create mock data object handler with data. + * + * @param array $mockData + * + * @return void + */ + private function mockDataObjectHandlerWithData(array $mockData): void + { + $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, $mockObjectManagerInstance); + } + + /** + * @inheritDoc */ public static function tearDownAfterClass(): void { + parent::tearDownAfterClass(); + + $dataObjectHandlerProperty = new ReflectionProperty(DataObjectHandler::class, "INSTANCE"); + $dataObjectHandlerProperty->setAccessible(true); + $dataObjectHandlerProperty->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/OperationDefinitionObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php index 35b06a24a..23dbcacc1 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php @@ -3,18 +3,19 @@ * 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 ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use tests\unit\Util\ObjectHandlerUtil; use tests\unit\Util\TestLoggingUtil; /** @@ -23,19 +24,25 @@ class OperationDefinitionObjectHandlerTest extends MagentoTestCase { /** - * Setup method + * @inheritDoc */ - public function setUp(): void + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } - public function testGetMultipleObjects() + /** + * 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 @@ -47,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" - ], ] - ]]]; - ObjectHandlerUtil::mockOperationHandlerWithData($mockData); + ] + ]; + $this->mockOperationHandlerWithData($mockData); //Perform Assertions $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -82,11 +93,17 @@ public function testGetMultipleObjects() $this->assertArrayHasKey($operationType2 . $dataType1, $operations); } - public function testDeprecatedOperation() + /** + * Validate testDeprecatedOperation. + * + * @return void + * @throws Exception + */ + public function testDeprecatedOperation(): void { // Data Variables for Assertions - $dataType1 = "type1"; - $operationType1 = "create"; + $dataType1 = 'type1'; + $operationType1 = 'create'; /** * Parser Output. Just one metadata with 1 field @@ -95,22 +112,25 @@ public function testDeprecatedOperation() * 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::OBJ_DEPRECATED => 'deprecation message' - ]]]; - ObjectHandlerUtil::mockOperationHandlerWithData($mockData); + OperationDefinitionObjectHandler::OBJ_DEPRECATED => 'deprecation message' + ] + ] + ]; + $this->mockOperationHandlerWithData($mockData); //Perform Assertions $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -119,7 +139,7 @@ public function testDeprecatedOperation() $this->assertArrayHasKey($operationType1 . $dataType1, $operations); TestLoggingUtil::getInstance()->validateMockLogStatement( 'notice', - "NOTICE: 1 metadata operation name violations detected. See mftf.log for details.", + 'NOTICE: 1 metadata operation name violations detected. See mftf.log for details.', [] ); // test run time deprecation notice @@ -127,33 +147,39 @@ public function testDeprecatedOperation() $operation->logDeprecated(); TestLoggingUtil::getInstance()->validateMockLogStatement( 'warning', - "DEPRECATION: The operation testOperationName is deprecated.", + 'DEPRECATION: The operation testOperationName is deprecated.', ['operationType' => 'create', 'deprecatedMessage' => 'deprecation message'] ); } - public function testObjectCreation() + /** + * 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"; + $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 @@ -171,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, @@ -229,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 - ObjectHandlerUtil::mockOperationHandlerWithData($mockData); + $this->mockOperationHandlerWithData($mockData); // Get Operation $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -264,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 @@ -283,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, @@ -325,7 +388,9 @@ public function testObjectArrayCreation() OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT, false, [], - [0 => $twoLevelNestedMetadata] + [ + 0 => $twoLevelNestedMetadata + ] ); $expectedOperation = new OperationElement( @@ -333,12 +398,14 @@ public function testObjectArrayCreation() $twiceNestedObjectType, $twiceNestedObjectKey, false, - [$twiceNestedObjectKey => $oneLevelNestedMetadata], + [ + $twiceNestedObjectKey => $oneLevelNestedMetadata + ], null ); // Set up mocked data output - ObjectHandlerUtil::mockOperationHandlerWithData($mockData); + $this->mockOperationHandlerWithData($mockData); // Get Operation $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -348,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 @@ -365,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, @@ -406,7 +481,7 @@ public function testLooseJsonCreation() ); // Set up mocked data output - ObjectHandlerUtil::mockOperationHandlerWithData($mockData); + $this->mockOperationHandlerWithData($mockData); // get Operations $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -418,10 +493,71 @@ public function testLooseJsonCreation() } /** - * clean up function runs after all tests + * 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); + 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 7e39fe216..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,8 +16,8 @@ use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; +use ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use tests\unit\Util\ObjectHandlerUtil; use tests\unit\Util\TestLoggingUtil; /** @@ -25,28 +26,31 @@ class PersistedObjectHandlerTest extends MagentoTestCase { /** - * Before test functionality - * @return void + * @inheritDoc */ - public function setUp(): void + 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 @@ -57,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' => [ @@ -78,15 +88,13 @@ public function testCreateSimpleEntity() ] ] ]; - $jsonResponse = " + $jsonResponse = " { \"" . strtolower($dataKey) . "\" : \"{$dataValue}\" } "; - // Mock Classes - ObjectHandlerUtil::mockDataObjectHandlerWithData($parserOutput); - $this->mockCurlHandler($jsonResponse); + $this->mockCurlHandler($jsonResponse, $parserOutput); $handler = PersistedObjectHandler::getInstance(); // Call method @@ -100,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' => [ @@ -121,15 +135,14 @@ public function testDeleteSimpleEntity() ] ] ]; - $jsonResponse = " + $jsonResponse = " { \"" . strtolower($dataKey) . "\" : \"{$dataValue}\" } "; // Mock Classes - ObjectHandlerUtil::mockDataObjectHandlerWithData($parserOutput); - $this->mockCurlHandler($jsonResponse); + $this->mockCurlHandler($jsonResponse, $parserOutput); $handler = PersistedObjectHandler::getInstance(); // Call method @@ -148,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' => [ @@ -169,15 +188,14 @@ public function testGetSimpleEntity() ] ] ]; - $jsonResponse = " + $jsonResponse = " { \"" . strtolower($dataKey) . "\" : \"{$dataValue}\" } "; // Mock Classes - ObjectHandlerUtil::mockDataObjectHandlerWithData($parserOutput); - $this->mockCurlHandler($jsonResponse); + $this->mockCurlHandler($jsonResponse, $parserOutput); $handler = PersistedObjectHandler::getInstance(); // Call method @@ -191,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' => [ @@ -224,7 +248,7 @@ public function testUpdateSimpleEntity() ] ] ]; - $jsonResponse = " + $jsonResponse = " { \"" . strtolower($dataKey) . "\" : \"{$dataValue}\" } @@ -236,15 +260,14 @@ public function testUpdateSimpleEntity() "; // Mock Classes - ObjectHandlerUtil::mockDataObjectHandlerWithData($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( @@ -257,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' => [ @@ -304,7 +333,7 @@ public function testRetrieveEntityAcrossScopes() ] ] ]; - $jsonReponseOne = " + $jsonResponseOne = " { \"" . strtolower($dataKeyOne) . "\" : \"{$dataValueOne}\" } @@ -323,22 +352,21 @@ public function testRetrieveEntityAcrossScopes() // Mock Classes and Create Entities $handler = PersistedObjectHandler::getInstance(); - ObjectHandlerUtil::mockDataObjectHandlerWithData($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, @@ -368,6 +396,8 @@ public function testRetrieveEntityAcrossScopes() } /** + * Validate testRetrieveEntityValidField. + * * @param string $name * @param string $key * @param string $value @@ -375,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 => [ @@ -391,7 +430,7 @@ public function testRetrieveEntityValidField($name, $key, $value, $type, $scope, ] ] ]; - $jsonReponseOne = " + $jsonResponseOne = " { \"" . strtolower($key) . "\" : \"{$value}\" } @@ -399,9 +438,7 @@ public function testRetrieveEntityValidField($name, $key, $value, $type, $scope, // Mock Classes and Create Entities $handler = PersistedObjectHandler::getInstance(); - - ObjectHandlerUtil::mockDataObjectHandlerWithData($parserOutputOne); - $this->mockCurlHandler($jsonReponseOne); + $this->mockCurlHandler($jsonResponseOne, $parserOutputOne); $handler->createEntity($stepKey, $scope, $name); // Call method @@ -411,6 +448,8 @@ public function testRetrieveEntityValidField($name, $key, $value, $type, $scope, } /** + * Validate testRetrieveEntityInValidField. + * * @param string $name * @param string $key * @param string $value @@ -418,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' => [ @@ -440,7 +486,7 @@ public function testRetrieveEntityInValidField($name, $key, $value, $type, $scop ] ] ]; - $jsonReponseOne = " + $jsonResponseOne = " { \"" . strtolower($key) . "\" : \"{$value}\" } @@ -448,8 +494,7 @@ public function testRetrieveEntityInValidField($name, $key, $value, $type, $scop // Mock Classes and Create Entities $handler = PersistedObjectHandler::getInstance(); - ObjectHandlerUtil::mockDataObjectHandlerWithData($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'], @@ -475,33 +522,79 @@ public static function entityDataProvider() ]; } - public function mockCurlHandler($response) - { - AspectMock::double(CurlHandler::class, [ - "__construct" => null, - "executeRequest" => $response, - "getRequestDataArray" => [], - "isContentTypeJson" => true - ]); - } - - public function tearDown(): void + /** + * Create mock curl handler. + * + * @param string $response + * @param array $parserOutput + * + * @return void + */ + public function mockCurlHandler(string $response, array $parserOutput): void { - // Clear out Singleton between tests - $property = new \ReflectionProperty(PersistedObjectHandler::class, "INSTANCE"); - $property->setAccessible(true); - $property->setValue(null); - - parent::tearDown(); // TODO: Change the autogenerated stub + $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); } /** - * After class functionality + * After class functionality. + * * @return void */ public static function tearDownAfterClass(): void { - TestLoggingUtil::getInstance()->clearMockLoggingUtil(); parent::tearDownAfterClass(); + + // Clear out Singleton between tests + $persistedObjectHandlerProperty = new ReflectionProperty(PersistedObjectHandler::class, "INSTANCE"); + $persistedObjectHandlerProperty->setAccessible(true); + $persistedObjectHandlerProperty->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/SecretStorage/AwsSecretsManagerStorageTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorageTest.php index 3112529c2..b37e22bd0 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorageTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorageTest.php @@ -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 62cb9798e..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\Exceptions\TestFrameworkException; +use ReflectionClass; +use ReflectionException; use tests\unit\Util\MagentoTestCase; -use AspectMock\Test as AspectMock; 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 3fc2aaacb..20c16428c 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php @@ -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"; } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php index 2721bd266..b4152901e 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php @@ -3,13 +3,15 @@ * 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 ReflectionProperty; use tests\unit\Util\MagentoTestCase; use tests\unit\Util\EntityDataObjectBuilder; use tests\unit\Util\OperationDefinitionBuilder; @@ -18,27 +20,28 @@ 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(): void @@ -54,8 +57,11 @@ public function setUp(): void * 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,55 +467,55 @@ public function testNestedMetadataArrayOfDiverseObjects() $this->assertEquals($expectedResult, $result); } - public function testExtendedWithRequiredEntity() + public function testExtendedWithRequiredEntity(): void { $entityDataObjectBuilder = new EntityDataObjectBuilder(); $extEntityDataObject = $entityDataObjectBuilder - ->withName("extEntity") - ->withType("entity") - ->withLinkedEntities(["baseSubentity" => "subentity","extSubentity" => "subentity"]) + ->withName('extEntity') + ->withType('entity') + ->withLinkedEntities(['baseSubentity' => 'subentity','extSubentity' => 'subentity']) ->build(); - $mockDOHInstance = AspectMock::double(DataObjectHandler::class, ["getObject" => function ($name) { + $callback = function ($name) { $entityDataObjectBuilder = new EntityDataObjectBuilder(); - if ($name == "baseSubentity") { + if ($name === 'baseSubentity') { return $entityDataObjectBuilder - ->withName("baseSubentity") - ->withType("subentity") - ->withDataFields(["subtest" => "BaseSubtest"]) + ->withName('baseSubentity') + ->withType('subentity') + ->withDataFields(['subtest' => 'BaseSubtest']) ->build(); } - if ($name == "extSubentity") { + if ($name === 'extSubentity') { return $entityDataObjectBuilder - ->withName("extSubentity") - ->withType("subentity") - ->withDataFields(["subtest" => "ExtSubtest"]) + ->withName('extSubentity') + ->withType('subentity') + ->withDataFields(['subtest' => 'ExtSubtest']) ->build(); } - }])->make(); - AspectMock::double(DataObjectHandler::class, ['getInstance' => $mockDOHInstance]); + }; + $this->mockDataObjectHandler($callback); $subentityOpElementBuilder = new OperationElementBuilder(); $subentityOpElement = $subentityOpElementBuilder - ->withKey("sub") - ->withType("subentity") - ->withElementType("object") - ->withFields(["subtest" => "string"]) + ->withKey('sub') + ->withType('subentity') + ->withElementType('object') + ->withFields(['subtest' => 'string']) ->build(); $operationResolver = new OperationDataArrayResolver(); $result = $operationResolver->resolveOperationDataArray( $extEntityDataObject, [$subentityOpElement], - "create", + 'create', false ); $expected = [ - "sub" => [ - "subtest" => "ExtSubtest" + 'sub' => [ + 'subtest' => 'ExtSubtest' ] ]; @@ -533,10 +523,61 @@ public function testExtendedWithRequiredEntity() } /** * After class functionality + * * @return void */ 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 3b79ffec3..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 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\ObjectManager; +use ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use AspectMock\Test as AspectMock; /** * Class DataExtensionUtilTest */ class DataExtensionUtilTest extends MagentoTestCase { - /** - * Before method functionality - * @return void - */ - protected function setUp(): void - { - 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/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 eb305bc4d..1f84f4b14 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php @@ -3,112 +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 ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use tests\unit\Util\ObjectHandlerUtil; use tests\unit\Util\TestLoggingUtil; +/** + * Class PageObjectHandlerTest + */ class PageObjectHandlerTest extends MagentoTestCase { /** - * Setup method + * @inheritDoc */ - public function setUp(): void + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } - public function testGetPageObject() + /** + * 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' ]]; - ObjectHandlerUtil::mockPageObjectHandlerWithData($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' ]]; - ObjectHandlerUtil::mockPageObjectHandlerWithData($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); } - public function testDeprecatedPage() + /** + * Validate testDeprecatedPage. + * + * @return void + * @throws XmlException + */ + public function testDeprecatedPage(): void { $mockData = [ - "testPage1" => [ - "url" => "testURL1", - "module" => "testModule1", - "section" => [ + 'testPage1' => [ + 'url' => 'testURL1', + 'module' => 'testModule1', + 'section' => [ ], - "area" => "test", - "deprecated" => "deprecation message", - "filename" => "filename.xml" + 'area' => 'test', + 'deprecated' => 'deprecation message', + 'filename' => 'filename.xml' ]]; - ObjectHandlerUtil::mockPageObjectHandlerWithData($mockData); - // get pages - $page = PageObjectHandler::getInstance()->getObject('testPage1'); + $this->mockPageObjectHandlerWithData($mockData); + PageObjectHandler::getInstance()->getObject('testPage1'); TestLoggingUtil::getInstance()->validateMockLogStatement( 'notice', - "NOTICE: 1 Page name violations detected. See mftf.log for details.", + 'NOTICE: 1 Page name violations detected. See mftf.log for details.', [] ); } /** - * clean up function runs after all tests + * Create mock page object handler with data. + * + * @param array $mockData + * + * @return void + */ + private function mockPageObjectHandlerWithData(array $mockData): void + { + $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, $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); + 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 69088944a..6ef16096a 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php @@ -3,100 +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 ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use tests\unit\Util\ObjectHandlerUtil; use tests\unit\Util\TestLoggingUtil; +/** + * Class SectionObjectHandlerTest + */ class SectionObjectHandlerTest extends MagentoTestCase { /** - * Setup method + * @inheritDoc */ - public function setUp(): void + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } - public function testGetSectionObject() + /** + * 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' ] ] ] ]; - ObjectHandlerUtil::mockSectionObjectHandlerWithData($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); } - public function testDeprecatedSection() + /** + * Validate testDeprecatedSection. + * + * @return void + * @throws XmlException + */ + public function testDeprecatedSection(): void { $mockData = [ - "testSection1" => [ - "element" => [ - "testElement" => [ - "type" => "input", - "selector" => "#element", - "deprecated" => "element deprecation message" + 'testSection1' => [ + 'element' => [ + 'testElement' => [ + 'type' => 'input', + 'selector' => '#element', + 'deprecated' => 'element deprecation message' ] ], - "filename" => "filename.xml", - "deprecated" => "section deprecation message" + 'filename' => 'filename.xml', + 'deprecated' => 'section deprecation message' ] ]; - ObjectHandlerUtil::mockSectionObjectHandlerWithData($mockData); + $this->mockSectionObjectHandlerWithData($mockData); // get sections $sectionHandler = SectionObjectHandler::getInstance(); - $section = $sectionHandler->getObject("testSection1"); + $sectionHandler->getObject('testSection1'); //validate deprecation warning TestLoggingUtil::getInstance()->validateMockLogStatement( 'notice', - "NOTICE: 1 Section name violations detected. See mftf.log for details.", + 'NOTICE: 1 Section name violations detected. See mftf.log for details.', [] ); } /** - * clean up function runs after all tests + * 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, $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); + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/DeprecatedEntityUsageCheckTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/DeprecatedEntityUsageCheckTest.php index bb4c96e89..57044f123 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/DeprecatedEntityUsageCheckTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/DeprecatedEntityUsageCheckTest.php @@ -3,37 +3,54 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\StaticCheck; +use InvalidArgumentException; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\DataGenerator\Parsers\OperationDefinitionParser; +use Magento\FunctionalTestingFramework\ObjectManager; +use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; use Magento\FunctionalTestingFramework\Page\Objects\PageObject; use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; use Magento\FunctionalTestingFramework\StaticCheck\DeprecatedEntityUsageCheck; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use ReflectionClass; +use ReflectionException; +use ReflectionProperty; use Symfony\Component\Console\Input\InputInterface; use tests\unit\Util\MagentoTestCase; -use ReflectionClass; -use InvalidArgumentException; -use tests\unit\Util\ObjectHandlerUtil; +/** + * Class DeprecatedEntityUsageCheckTest + */ class DeprecatedEntityUsageCheckTest extends MagentoTestCase { - /** @var DeprecatedEntityUsageCheck */ + /** @var DeprecatedEntityUsageCheck */ private $staticCheck; /** @var ReflectionClass*/ private $staticCheckClass; + /** + * @inheritDoc + */ public function setUp(): void { $this->staticCheck = new DeprecatedEntityUsageCheck(); - $this->staticCheckClass = new \ReflectionClass($this->staticCheck); + $this->staticCheckClass = new ReflectionClass($this->staticCheck); } - public function testInvalidPathOption() + /** + * Validate testInvalidPathOption. + * + * @return void + * @throws ReflectionException + */ + public function testInvalidPathOption(): void { $input = $this->getMockBuilder(InputInterface::class) ->disableOriginalConstructor() @@ -50,7 +67,13 @@ public function testInvalidPathOption() $loadAllXmlFiles->invoke($this->staticCheck, $input); } - public function testViolatingElementReferences() + /** + * Validate testViolatingElementReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingElementReferences(): void { //variables for assertions $elementName = 'elementOne'; @@ -73,13 +96,19 @@ public function testViolatingElementReferences() $this->assertEquals($actual, $expected); } - public function testViolatingPageReferences() + /** + * 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'); + $page = new PageObject($pageName, '/url.html', 'Test', [], false, 'test', $fileName, 'deprecated'); $references = ['Page' => $page]; $actual = $this->callViolatingReferences($references); $expected = [ @@ -93,7 +122,13 @@ public function testViolatingPageReferences() $this->assertEquals($actual, $expected); } - public function testViolatingDataReferences() + /** + * Validate testViolatingDataReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingDataReferences(): void { //Data entity variables for assertions $entityName = 'EntityOne'; @@ -123,7 +158,13 @@ public function testViolatingDataReferences() $this->assertEquals($actual, $expected); } - public function testViolatingTestReferences() + /** + * Validate testViolatingTestReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingTestReferences(): void { // test variables for assertions $testName = 'Test1'; @@ -143,12 +184,18 @@ public function testViolatingTestReferences() $this->assertEquals($actual, $expected); } - public function testViolatingMetaDataReferences() + /** + * Validate testViolatingMetaDataReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingMetaDataReferences(): void { // Data Variables for Assertions - $dataType1 = "type1"; - $operationType1 = "create"; - $operationType2 = "update"; + $dataType1 = 'type1'; + $operationType1 = 'create'; + $operationType2 = 'update'; /** * Parser Output. @@ -161,34 +208,34 @@ public function testViolatingMetaDataReferences() * key=id, value=integer */ $mockData = [OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ - "testOperationName" => [ + '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_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_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_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_ENTRY_KEY => 'id', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => 'integer' ], ] ]]]; - ObjectHandlerUtil::mockOperationHandlerWithData($mockData); + $this->mockOperationHandlerWithData($mockData); $dataName = 'dataName1'; $references = [ $dataName => [ @@ -213,7 +260,13 @@ public function testViolatingMetaDataReferences() $this->assertEquals($actual, $expected); } - public function testIsDeprecated() + /** + * Validate testIsDeprecated. + * + * @return void + * @throws ReflectionException + */ + public function testIsDeprecated(): void { // Test Data $contents = ' @@ -231,15 +284,85 @@ public function testIsDeprecated() } /** - * Invoke findViolatingReferences - * @param $references + * 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 + * @throws ReflectionException */ - public function callViolatingReferences($references) + 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 6f337b05c..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. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Suite\Handlers; -use AspectMock\Test as AspectMock; -use Magento\FunctionalTestingFramework\ObjectManager\ObjectManager; -use Magento\FunctionalTestingFramework\ObjectManagerFactory; +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 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(): void - { - $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 136b32e2a..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. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Suite; -use AspectMock\Test as AspectMock; +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; @@ -18,41 +21,86 @@ use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Magento\FunctionalTestingFramework\Util\Manifest\DefaultTestManifest; +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(): void + 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(): void + 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 + * 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,20 +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 + * 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 @@ -108,22 +159,25 @@ 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 + * 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 @@ -142,17 +196,20 @@ public function testGenerateEmptySuite() $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); // set expected error message - $this->expectExceptionMessage("Suite basicTestSuite is not defined in xml or is invalid"); + $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'); } /** - * Tests generating all suites with a suite containing invalid test reference + * Tests generating all suites with a suite containing invalid test reference. + * + * @return void + * @throws TestReferenceException */ - public function testInvalidSuiteTestPair() + public function testInvalidSuiteTestPair(): void { // Mock Suite1 => Test1 and Suite2 => Test2 $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); @@ -198,9 +255,12 @@ public function testInvalidSuiteTestPair() } /** - * Tests generating all suites with a non-existing suite + * Tests generating all suites with a non-existing suite. + * + * @return void + * @throws TestReferenceException */ - public function testNonExistentSuiteTestPair() + public function testNonExistentSuiteTestPair(): void { $testDataArrayBuilder = new TestDataArrayBuilder(); $mockSimpleTest = $testDataArrayBuilder @@ -227,9 +287,12 @@ public function testNonExistentSuiteTestPair() } /** - * Tests generating split suites for parallel test generation + * Tests generating split suites for parallel test generation. + * + * @return void + * @throws TestReferenceException */ - public function testGenerateSplitSuiteFromTest() + public function testGenerateSplitSuiteFromTest(): void { $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); $mockSuiteData = $suiteDataArrayBuilder @@ -272,8 +335,8 @@ public function testGenerateSplitSuiteFromTest() // assert last split suite group generated TestLoggingUtil::getInstance()->validateMockLogStatement( 'info', - "suite generated", - ['suite' => 'mockSuite_1_G', 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . "mockSuite_1_G"] + 'suite generated', + ['suite' => 'mockSuite_1_G', 'relative_path' => '_generated' . DIRECTORY_SEPARATOR . 'mockSuite_1_G'] ); } @@ -282,75 +345,127 @@ public function testGenerateSplitSuiteFromTest() * * @param array $testData * @param array $suiteData - * @throws \Exception + * + * @return void + * @throws Exception + */ + private function setMockTestAndSuiteParserOutput(array $testData, array $suiteData): void + { + $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 setMockTestAndSuiteParserOutput($testData, $suiteData) + private function clearMockResolverProperties(): void { - $property = new \ReflectionProperty(SuiteGenerator::class, 'instance'); + $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 = 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->setAccessible(true); - $property->setValue($instance, $instance); + $property->setValue(null, null); } /** - * clean up function runs after each test + * @inheritDoc */ - public function tearDown(): void + protected function tearDown(): void { GenerationErrorHandler::getInstance()->reset(); } /** - * clean up function runs after all tests + * @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/Handlers/ActionGroupObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/ActionGroupObjectHandlerTest.php index 71c1833ba..ce5983f62 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/ActionGroupObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/ActionGroupObjectHandlerTest.php @@ -3,28 +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; -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 tests\unit\Util\MagentoTestCase; -use tests\unit\Util\ActionGroupArrayBuilder; use Magento\FunctionalTestingFramework\Test\Parsers\ActionGroupDataParser; -use tests\unit\Util\ObjectHandlerUtil; +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'; @@ -35,21 +39,22 @@ public function testGetTestObjectWithInvalidExtends() ->withFilename() ->withActionObjects() ->build(); - ObjectHandlerUtil::mockActionGroupObjectHandlerWithData(['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'; @@ -69,7 +74,7 @@ public function testGetAllTestObjectsWithInvalidExtends() ->withActionObjects() ->build(); - ObjectHandlerUtil::mockActionGroupObjectHandlerWithData( + $this->mockActionGroupObjectHandlerWithData( [ 'actionGroups' => array_merge( $actionGroupOne, @@ -80,8 +85,68 @@ public function testGetAllTestObjectsWithInvalidExtends() $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(); } + + /** + * Create mock action group object handler with data. + * + * @param array $mockData + * + * @return void + */ + private function mockActionGroupObjectHandlerWithData(array $mockData): void + { + $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, $mockObjectManagerInstance); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + $actionGroupObjectHandlerProperty = new ReflectionProperty(ActionGroupObjectHandler::class, 'instance'); + $actionGroupObjectHandlerProperty->setAccessible(true); + $actionGroupObjectHandlerProperty->setValue(null, null); + + $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 6d259cca2..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; - +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,16 +18,24 @@ use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; +use Magento\FunctionalTestingFramework\Util\ModuleResolver; +use ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use tests\unit\Util\ObjectHandlerUtil; use tests\unit\Util\TestDataArrayBuilder; -use tests\unit\Util\MockModuleResolverBuilder; use tests\unit\Util\TestLoggingUtil; -use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; +use Magento\FunctionalTestingFramework\Filter\FilterList; +use Magento\FunctionalTestingFramework\Util\Script\TestDependencyUtil; class TestObjectHandlerTest extends MagentoTestCase { - public function setUp(): void + /** + * Before test functionality. + * + * @return void + * @throws Exception + */ + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } @@ -33,9 +43,10 @@ public function setUp(): void /** * 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(); @@ -47,13 +58,10 @@ public function testGetTestObject() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - ObjectHandlerUtil::mockTestObjectHandlerWitData($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 @@ -114,9 +122,11 @@ 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'); } @@ -124,9 +134,10 @@ public function testGetTestWithFileName() /** * 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()) @@ -140,9 +151,7 @@ public function testGetTestsByGroup() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - ObjectHandlerUtil::mockTestObjectHandlerWitData(array_merge($includeTest, $excludeTest)); + $this->mockTestObjectHandler(array_merge($includeTest, $excludeTest)); // execute test method $toh = TestObjectHandler::getInstance(); @@ -155,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"; @@ -188,10 +198,8 @@ public function testGetTestWithModuleName() ->withFileName($file) ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(['Vendor_' . $moduleExpected => $filepath]); + $this->mockTestObjectHandler($mockData, ['Vendor_' . $moduleExpected => $filepath]); - ObjectHandlerUtil::mockTestObjectHandlerWitData($mockData); // Execute Test Method $toh = TestObjectHandler::getInstance(); $actualTestObject = $toh->getObject($testDataArrayBuilder->testName); @@ -201,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()) @@ -217,24 +226,23 @@ public function testGetTestObjectWithInvalidExtends() ->withBeforeHook() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - ObjectHandlerUtil::mockTestObjectHandlerWitData($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()) @@ -255,9 +263,7 @@ public function testGetAllTestObjectsWithInvalidExtends() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - ObjectHandlerUtil::mockTestObjectHandlerWitData(array_merge($testOne, $testTwo)); + $this->mockTestObjectHandler(array_merge($testOne, $testTwo)); $toh = TestObjectHandler::getInstance(); $toh->getAllObjects(); @@ -270,11 +276,12 @@ public function testGetAllTestObjectsWithInvalidExtends() } /** - * Validate test object when ENABLE_PAUSE is set to true + * Validate test object when ENABLE_PAUSE is set to true. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetTestObjectWhenEnablePause() + public function testGetTestObjectWhenEnablePause(): void { // set up mock data putenv('ENABLE_PAUSE=true'); @@ -287,13 +294,10 @@ public function testGetTestObjectWhenEnablePause() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - ObjectHandlerUtil::mockTestObjectHandlerWitData($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 @@ -363,14 +367,137 @@ public function testGetTestObjectWhenEnablePause() } /** - * After method functionality + * After method functionality. * * @return void */ - public function tearDown(): void + protected function tearDown(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); - AspectMock::clean(); parent::tearDownAfterClass(); } + + /** + * Mock test object handler. + * + * @param array $data + * @param array|null $paths + * + * @return void + */ + 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->setAccessible(true); + $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); + + $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'); + } + + /** + * Basic test when no filter applied + * + * @return void + * @throws Exception + */ + public function testGetFilteredTestNamesWhenNoFilterIsApplied() + { + $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 4424989f8..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 tests\unit\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(): void + 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,13 +332,20 @@ 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')]) @@ -249,8 +358,10 @@ public function testExceptionOnMissingActions() /** * 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')]) @@ -262,10 +373,12 @@ public function testExceptionOnMissingArguments() } /** - * 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,7 +484,8 @@ private function assertOnMergeKeyAndActionValue($actions, $expectedValue, $expec } /** - * After class functionality + * After class functionality. + * * @return void */ public static function tearDownAfterClass(): void diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php index 8bf71d542..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 tests\unit\Util\TestLoggingUtil; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use ReflectionProperty; use tests\unit\Util\MagentoTestCase; +use tests\unit\Util\TestLoggingUtil; /** * Class ActionObjectTest @@ -25,41 +28,51 @@ class ActionObjectTest extends MagentoTestCase { /** - * Before test functionality + * Before test functionality. + * * @return void */ - public function setUp(): void + 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,36 +442,68 @@ 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(): void diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php index a3d2cca86..23c97fd4a 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php @@ -3,50 +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\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(): void + 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(): void { diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php index 8c55210a4..8604db683 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,16 +14,18 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; +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(): void + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } @@ -31,8 +34,10 @@ public function setUp(): void * 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; @@ -46,7 +51,6 @@ public function testResolveActionStepOrdering() $stepKey = 'stepKey'. $i; $type = 'testType'; $actionAttributes = []; - $actions[] = new ActionObject($stepKey, $type, $actionAttributes); } @@ -93,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"; @@ -110,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', @@ -149,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); } @@ -158,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); @@ -185,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']); } @@ -212,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']); } @@ -239,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']); } @@ -266,20 +276,21 @@ 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( - 'You cannot reference secret data outside of the fillField, magentoCLI and createData actions' + 'You cannot reference secret data outside of the fillField, magentoCLI, seeInField and createData actions' ); $actionObjectOne = new ActionObject( 'actionKey1', 'click', - ['userInput' => '{{_CREDS.username}}'] + ['userInput' => '{{_CREDS.username}}','requiredCredentials' => 'username'] ); $actionObject = [$actionObjectOne]; @@ -288,11 +299,39 @@ public function testInvalidSecretFunctions() } /** - * After class functionality + * After class functionality. + * * @return void */ public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } + + /** + * Verify that a action is replaced by when secret _CREDS are referenced. + * + * @return void + * @throws TestReferenceException + * @throws XmlException + */ + public function testValidSeeInSecretFieldFunction(): void + { + $actionObjectOne = new ActionObject( + 'actionKey1', + 'seeInField', + ['userInput' => '{{_CREDS.username}}', 'requiredCredentials' => 'username'] + ); + $actionObject = [$actionObjectOne]; + + $actionMergeUtil = new ActionMergeUtil('actionMergeUtilTest', 'TestCase'); + $result = $actionMergeUtil->resolveActionSteps($actionObject); + + $expectedValue = new ActionObject( + 'actionKey1', + 'seeInSecretField', + ['userInput' => '{{_CREDS.username}}','requiredCredentials' => 'username'] + ); + $this->assertEquals($expectedValue, $result['actionKey1']); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php index 735c33492..44ec3a7aa 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php @@ -3,114 +3,119 @@ * 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; -use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; +/** + * Class AnnotationExtractorTest + */ class AnnotationExtractorTest extends TestCase { /** - * Before test functionality - * @return void + * @inheritDoc */ - public function setUp(): void + 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 + * Annotation extractor should throw warning when required annotations are missing. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testMissingAnnotations() + public function testMissingAnnotations(): void { // Test Data, missing title, description, and severity $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' ] ], - "group" => [ + 'group' => [ [ - "nodeName" => "group", - "value" => "TestGroup" + 'nodeName' => 'group', + 'value' => 'TestGroup' ] ], ]; // Perform Test $extractor = new AnnotationExtractor(); - $returnedAnnotations = $extractor->extractAnnotations($testAnnotations, "testFileName"); + $extractor->extractAnnotations($testAnnotations, 'testFileName'); // Asserts TestLoggingUtil::getInstance()->validateMockLogStatement( @@ -118,61 +123,62 @@ public function testMissingAnnotations() 'DEPRECATION: Test testFileName is missing required annotations.', [ 'testName' => 'testFileName', - 'missingAnnotations' => "title, description, severity" + 'missingAnnotations' => 'title, description, severity' ] ); } /** - * Annotation extractor should throw warning when required annotations are empty + * Annotation extractor should throw warning when required annotations are empty. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testEmptyRequiredAnnotations() + public function testEmptyRequiredAnnotations(): void { // Test Data, missing title, description, and severity $testAnnotations = [ - "nodeName" => "annotations", - "features" => [ + 'nodeName' => 'annotations', + 'features' => [ [ - "nodeName" => "features", - "value" => "" + 'nodeName' => 'features', + 'value' => '' ] ], - "stories" => [ + 'stories' => [ [ - "nodeName" => "stories", - "value" => "TestStories" + 'nodeName' => 'stories', + 'value' => 'TestStories' ] ], - "title" => [ + 'title' => [ [ - "nodeName" => "title", - "value" => " " + 'nodeName' => 'title', + 'value' => ' ' ] ], - "description" => [ + 'description' => [ [ - "nodeName" => "description", - "value" => "\t" + 'nodeName' => 'description', + 'value' => "\t" ] ], - "severity" => [ + 'severity' => [ [ - "nodeName" => "severity", - "value" => "" + 'nodeName' => 'severity', + 'value' => '' ] ], - "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 TestLoggingUtil::getInstance()->validateMockLogStatement( @@ -180,90 +186,96 @@ public function testEmptyRequiredAnnotations() '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(); // 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'") + . preg_quote('TestCaseId and Title pairs is not unique in Tests \'firstTest\', \'secondTest\'') . '/'; TestLoggingUtil::getInstance()->validateMockLogStatmentRegex('error', $errorMessage, []); $testErrors = GenerationErrorHandler::getInstance()->getErrorsByType('test'); @@ -271,14 +283,16 @@ public function testTestCaseIdUniqueness() $this->assertArrayHasKey('secondTest', $testErrors); } - public function tearDown(): void + /** + * @inheritDoc + */ + protected function tearDown(): void { GenerationErrorHandler::getInstance()->reset(); } /** - * After class functionality - * @return void + * @inheritDoc */ public static function tearDownAfterClass(): void { diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php index 6bf2da577..466edc322 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php @@ -3,40 +3,37 @@ * 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(): void + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); } /** - * After class functionality + * After class functionality. + * * @return void */ public static function tearDownAfterClass(): void @@ -45,13 +42,15 @@ public static function tearDownAfterClass(): void } /** - * 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,31 +338,32 @@ 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 + * Tests generating a test that extends a skipped parent test. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testExtendedTestSkippedParent() + public function testExtendedTestSkippedParent(): void { $testDataArrayBuilder = new TestDataArrayBuilder(); $mockParentTest = $testDataArrayBuilder @@ -364,7 +376,7 @@ public function testExtendedTestSkippedParent() $testDataArrayBuilder->reset(); $mockExtendedTest = $testDataArrayBuilder ->withName('extendTest') - ->withTestReference("baseTest") + ->withTestReference('baseTest') ->build(); $mockTestData = array_merge($mockParentTest, $mockExtendedTest); @@ -376,7 +388,7 @@ public function testExtendedTestSkippedParent() // validate log statement TestLoggingUtil::getInstance()->validateMockLogStatement( 'debug', - "extendTest is skipped due to ParentTestIsSkipped", + 'extendTest is skipped due to ParentTestIsSkipped', [] ); } @@ -384,43 +396,55 @@ public function testExtendedTestSkippedParent() /** * 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/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 cec5f097e..39f8c555d 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ComposerModuleResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ComposerModuleResolverTest.php @@ -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 index 470d84041..33cc78eee 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/GenerationErrorHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/GenerationErrorHandlerTest.php @@ -10,7 +10,6 @@ use ReflectionProperty; use tests\unit\Util\MagentoTestCase; use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; -use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; /** * Class GenerationErrorHandlerTest @@ -20,12 +19,8 @@ class GenerationErrorHandlerTest extends MagentoTestCase /** * Test get errors when all errors are distinct */ - public function testGetDistinctErrors() + public function testGetDistinctErrors():void { - $this->createMock(MftfApplicationConfig::class) - ->method('getPhase') - ->willReturn(MftfApplicationConfig::GENERATION_PHASE); - $expectedAllErrors = [ 'test' => [ 'Sameple1Test' => [ @@ -77,12 +72,8 @@ public function testGetDistinctErrors() /** * Test get errors when some errors have the same key */ - public function testGetErrorsWithSameKey() + public function testGetErrorsWithSameKey(): void { - $this->createMock(MftfApplicationConfig::class) - ->method('getPhase') - ->willReturn(MftfApplicationConfig::GENERATION_PHASE); - $expectedAllErrors = [ 'test' => [ 'Sameple1Test' => [ @@ -160,12 +151,8 @@ public function testGetErrorsWithSameKey() /** * Test get errors when some errors are duplicate */ - public function testGetAllErrorsDuplicate() + public function testGetAllErrorsDuplicate(): void { - $this->createMock(MftfApplicationConfig::class) - ->method('getPhase') - ->willReturn(MftfApplicationConfig::GENERATION_PHASE); - $expectedAllErrors = [ 'test' => [ 'Sameple1Test' => [ @@ -245,9 +232,11 @@ public function testGetAllErrorsDuplicate() * * @param string $expectedErrMessages * @param array $errors + * + * @return void * @dataProvider getAllErrorMessagesDataProvider */ - public function testGetAllErrorMessages($expectedErrMessages, $errors) + public function testGetAllErrorMessages(string $expectedErrMessages, array $errors): void { $handler = GenerationErrorHandler::getInstance(); $handler->reset(); @@ -265,7 +254,7 @@ public function testGetAllErrorMessages($expectedErrMessages, $errors) * * @return array */ - public function getAllErrorMessagesDataProvider() + public static function getAllErrorMessagesDataProvider(): array { return [ ['', []], @@ -330,12 +319,8 @@ public function getAllErrorMessagesDataProvider() /** * Test reset */ - public function testResetError() + public function testResetError(): void { - $this->createMock(MftfApplicationConfig::class) - ->method('getPhase') - ->willReturn(MftfApplicationConfig::GENERATION_PHASE); - GenerationErrorHandler::getInstance()->addError('something', 'some', 'error'); GenerationErrorHandler::getInstance()->addError('otherthing', 'other', 'error'); GenerationErrorHandler::getInstance()->reset(); @@ -348,10 +333,13 @@ public function testResetError() $this->assertEquals([], GenerationErrorHandler::getInstance()->getErrorsByType('nothing')); } + /** + * @inheritdoc + */ public function tearDown(): void { $property = new ReflectionProperty(GenerationErrorHandler::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); + $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 3f2ba1962..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\Util; +use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\Util\ModulePathExtractor; +use Magento\FunctionalTestingFramework\Util\ModuleResolver; +use ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use tests\unit\Util\MockModuleResolverBuilder; 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 f43f82fe2..9809e50ff 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php @@ -3,81 +3,94 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\Util; -use AspectMock\Proxy\Verifier; -use AspectMock\Test as AspectMock; - +use Exception; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; use Magento\FunctionalTestingFramework\Exceptions\FastFailException; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Magento\FunctionalTestingFramework\ObjectManager; -use Magento\FunctionalTestingFramework\ObjectManagerFactory; -use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Magento\FunctionalTestingFramework\Util\ModuleResolver\ModuleResolverService; use Magento\FunctionalTestingFramework\Util\ModuleResolver; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionProperty; use tests\unit\Util\MagentoTestCase; -use PHPUnit\Runner\Exception; use tests\unit\Util\TestLoggingUtil; +/** + * Class ModuleResolverTest + */ class ModuleResolverTest extends MagentoTestCase { /** - * Before test functionality - * @return void + * @inheritDoc */ - public function setUp(): void + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * After class functionality - * @return void + * @inheritDoc */ 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( @@ -90,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( @@ -236,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 @@ -284,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, @@ -336,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, @@ -435,7 +470,7 @@ public function testMergeFlipAndFilterModulePathsNoForceGenerate() 4 => 'Magento_ModuleB', 5 => 'Magento_ModuleD', 6 => 'Magento_Otherexample', - 7 => 'Magento_ModuleC', + 7 => 'Magento_ModuleC' ] ); $this->assertEquals( @@ -446,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() @@ -454,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, @@ -529,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, @@ -609,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, @@ -666,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', @@ -709,35 +761,24 @@ public function testApplyCustomModuleMethods() * Validate blocklisted modules are removed * Module paths are sorted according to module name in alphabetically ascending order * - * @throws \Exception + * @throws Exception */ - public function testGetModulePathsBlocklist() + 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( @@ -752,69 +793,70 @@ 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 + // 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(FastFailException::class); @@ -823,15 +865,18 @@ public function testGetAdminTokenWithMissingEnv() /** * 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(FastFailException::class); @@ -839,162 +884,77 @@ public function testGetAdminTokenWithBadResponse() } /** - * 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 $mockBlocklist - * @throws \Exception + * @param MockObject|null $moduleResolverService + * + * @return void */ - private function setMockResolverProperties($instance, $mockPaths = null, $mockModules = null, $mockBlocklist = []) + 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, 'moduleBlocklist'); + $property = new ReflectionProperty(ModuleResolverService::class, 'INSTANCE'); $property->setAccessible(true); - $property->setValue($instance, $mockBlocklist); + $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(): 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 1bad633e2..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. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Util\Path; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use tests\unit\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 48fc9f337..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. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Util\Path; -use tests\unit\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 6b25a837a..7bd29acab 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php @@ -3,23 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Util\Sorter; -use AspectMock\Test as AspectMock; use Magento\FunctionalTestingFramework\Exceptions\FastFailException; -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\Sorter\ParallelGroupSorter; +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 testBasicTestsSplitByTime() + public function testBasicTestsSplitByTime(): void { $sampleTestArray = [ 'test1' => 100, @@ -44,41 +47,24 @@ public function testBasicTestsSplitByTime() $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 testTestsAndSuitesSplitByTime() + 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 = [ @@ -115,9 +101,12 @@ public function testTestsAndSuitesSplitByTime() } /** - * Test splitting tests based on a fixed group number + * Test splitting tests based on a fixed group number. + * + * @return void + * @throws FastFailException */ - public function testBasicTestsSplitByGroup() + public function testBasicTestsSplitByGroup(): void { $sampleTestArray = [ 'test1' => 100, @@ -140,39 +129,41 @@ public function testBasicTestsSplitByGroup() 'test18' => 34, 'test19' => 45, 'test20' => 58, - 'test21' => 9, + 'test21' => 9 ]; $expectedResult = [ 1 => ['test2', 'test8'], - 2 => ['test11', 'test9', 'test17', 'test19', 'test13'], - 3 => ['test7', 'test18', 'test14', 'test21'], - 4 => ['test6', 'test12', 'test20', 'test5', 'test10'], - 5 => ['test1', 'test16', 'test4', 'test3', 'test15'] + 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 $gropuNumber => $actualTests) { - $expectedTests = $expectedResult[$gropuNumber]; + 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 + * Test splitting tests based a group number bigger than ever needed. + * + * @return void + * @throws FastFailException */ - public function testBasicTestsSplitByBigGroupNumber() + public function testBasicTestsSplitByBigGroupNumber(): void { $sampleTestArray = [ 'test1' => 100, 'test2' => 300, 'test3' => 50, 'test4' => 60, - 'test5' => 25, + 'test5' => 25 ]; $expectedResult = [ @@ -185,19 +176,21 @@ public function testBasicTestsSplitByBigGroupNumber() $testSorter = new ParallelGroupSorter(); $actualResult = $testSorter->getTestsGroupedByFixedGroupCount([], $sampleTestArray, 10); - $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 splitting tests based a minimum group number + * Test splitting tests based a minimum group number. + * + * @return void + * @throws FastFailException */ - public function testBasicTestsSplitByMinGroupNumber() + public function testBasicTestsSplitByMinGroupNumber(): void { $sampleTestArray = [ 'test1' => 100, @@ -213,41 +206,24 @@ public function testBasicTestsSplitByMinGroupNumber() $testSorter = new ParallelGroupSorter(); $actualResult = $testSorter->getTestsGroupedByFixedGroupCount([], $sampleTestArray, 1); - $this->assertCount(1, $actualResult); - foreach ($actualResult as $gropuNumber => $actualTests) { - $expectedTests = $expectedResult[$gropuNumber]; + 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 + * Test splitting tests and suites based on a fixed group number. + * + * @return void + * @throws FastFailException */ - public function testTestsAndSuitesSplitByGroup() + public function testTestsAndSuitesSplitByGroup(): void { // mock tests for test object handler. - $numberOfCalls = 0; - $mockTest1 = AspectMock::double( - TestObject::class, - ['getEstimatedDuration' => function () use (&$numberOfCalls) { - $actionCount = [300, 275, 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); // create test to size array $sampleTestArray = [ @@ -283,7 +259,7 @@ public function testTestsAndSuitesSplitByGroup() 'test30' => 93, 'test31' => 330, 'test32' => 85, - 'test33' => 291, + 'test33' => 291 ]; // create mock suite references @@ -294,26 +270,25 @@ public function testTestsAndSuitesSplitByGroup() // perform sort $testSorter = new ParallelGroupSorter(); $actualResult = $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 15); - // verify the resulting groups $this->assertCount(15, $actualResult); $expectedResults = [ - 1 => ['test31', 'test8', 'test1'], + 1 => ['test31', 'test3'], 2 => ['test6', 'test5'], 3 => ['test33', 'test17'], 4 => ['test25', 'test32'], 5 => ['test7', 'test22'], 6 => ['test10', 'test30'], 7 => ['test29', 'test12'], - 8 => ['test13', 'test11', 'test3'], + 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'], + 15 => ['mockSuite1_1_G'] ]; foreach ($actualResult as $groupNum => $group) { @@ -322,37 +297,21 @@ public function testTestsAndSuitesSplitByGroup() } /** - * Test splitting tests and suites based a group number bigger than ever needed + * Test splitting tests and suites based a group number bigger than ever needed. + * + * @return void + * @throws FastFailException */ - public function testTestsAndSuitesSplitByBigGroupNumber() + public function testTestsAndSuitesSplitByBigGroupNumber(): void { // mock tests for test object handler. - $numberOfCalls = 0; - $mockTest1 = AspectMock::double( - TestObject::class, - ['getEstimatedDuration' => function () use (&$numberOfCalls) { - $actionCount = [300, 275, 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); // create test to size array $sampleTestArray = [ 'test1' => 275, 'test2' => 190, - 'test3' => 200, + 'test3' => 200 ]; // create mock suite references @@ -363,7 +322,6 @@ public function testTestsAndSuitesSplitByBigGroupNumber() // perform sort $testSorter = new ParallelGroupSorter(); $actualResult = $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 10); - // verify the resulting groups $this->assertCount(5, $actualResult); @@ -381,37 +339,21 @@ public function testTestsAndSuitesSplitByBigGroupNumber() } /** - * Test splitting tests and suites based a minimum group number + * Test splitting tests and suites based a minimum group number. + * + * @return void + * @throws FastFailException */ - public function testTestsAndSuitesSplitByMinGroupNumber() + public function testTestsAndSuitesSplitByMinGroupNumber(): void { // mock tests for test object handler. - $numberOfCalls = 0; - $mockTest1 = AspectMock::double( - TestObject::class, - ['getEstimatedDuration' => function () use (&$numberOfCalls) { - $actionCount = [300, 275, 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); // create test to size array $sampleTestArray = [ 'test1' => 1, 'test2' => 125, - 'test3' => 35, + 'test3' => 35 ]; // create mock suite references @@ -422,14 +364,13 @@ public function testTestsAndSuitesSplitByMinGroupNumber() // 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'], + 3 => ['mockSuite1_1_G'] ]; foreach ($actualResult as $groupNum => $group) { @@ -438,49 +379,155 @@ public function testTestsAndSuitesSplitByMinGroupNumber() } /** - * Test splitting tests and suites with invalid group number + * 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 testTestsAndSuitesSplitByInvalidGroupNumber() + public function testTestsAndSuitesSplitByGroupNumberSuiteNoTest(): void { // mock tests for test object handler. - $numberOfCalls = 0; - $mockTest1 = AspectMock::double( - TestObject::class, - ['getEstimatedDuration' => function () use (&$numberOfCalls) { - $actionCount = [300, 275, 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); // create test to size array $sampleTestArray = [ 'test1' => 1, 'test2' => 125, - 'test3' => 35, + 'test3' => 35 ]; // create mock suite references $sampleSuiteArray = [ - 'mockSuite1' => ['mockTest1', 'mockTest2'] + '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 2"); + $this->expectExceptionMessage("Invalid parameter 'groupTotal': must be equal or greater than 3"); // perform sort $testSorter = new ParallelGroupSorter(); - $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 1); + $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 8817d89c7..781e69584 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/TestGeneratorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/TestGeneratorTest.php @@ -3,77 +3,177 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\Util; -use AspectMock\Test as AspectMock; - +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 tests\unit\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\Util\GenerationErrorHandler; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; class TestGeneratorTest extends MagentoTestCase { /** - * Before method functionality + * @inheritdoc */ - public function setUp(): void + 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 + */ + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * After method functionality + * After method functionality. * * @return void */ - public function tearDown(): void + protected function tearDown(): void { - AspectMock::clean(); 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"); - - AspectMock::double(TestObjectHandler::class, ['initTestData' => '']); - - $testGeneratorObject = TestGenerator::getInstance("", ["sampleTest" => $testObject]); - - AspectMock::double(TestGenerator::class, ['loadAllTestObjects' => ["sampleTest" => $testObject]]); + $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") . '/'; + $errorMessage = '/' . preg_quote('Removed invalid test object sampleTest') . '/'; TestLoggingUtil::getInstance()->validateMockLogStatmentRegex('error', $errorMessage, []); $testErrors = GenerationErrorHandler::getInstance()->getErrorsByType('test'); $this->assertArrayHasKey('sampleTest', $testErrors); } /** - * Tests that skipped tests do not have a fully generated body + * 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}}' + ]); + + $testObject = new TestObject('sampleTest', ['merge123' => $actionObject], [], [], 'filename'); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); + + $result = $testGeneratorObject->getUniqueIdForInput('prefix', "foo"); + + $this->assertMatchesRegularExpression('/[A-Za-z0-9]+foo/', $result); + } + + /** + * 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}}' + ]); + + $testObject = new TestObject('sampleTest', ['merge123' => $actionObject], [], [], 'filename'); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); + $this->expectException(TestReferenceException::class); + $result = $testGeneratorObject->entityExistsCheck('testintity', "teststepkey"); + } + + /** + * Basic test to check unique id is appended to input as suffix + * + * @return void + * @throws Exception + */ + 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. * - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @return void + * @throws TestReferenceException */ - public function testSkippedNoGeneration() + public function testSkippedNoGeneration(): void { $actionInput = 'fakeInput'; $actionObject = new ActionObject('fakeAction', 'comment', [ @@ -81,9 +181,9 @@ 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->assertStringContainsString('This test is skipped', $output); @@ -91,14 +191,22 @@ public function testSkippedNoGeneration() } /** - * 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', [ @@ -110,16 +218,16 @@ 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->assertStringNotContainsString('This test is skipped', $output); @@ -128,17 +236,22 @@ public function testAllowSkipped() } /** - * 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', [ @@ -148,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/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 7760acfc6..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 tests\unit\Util; -use AspectMock\Test as AspectMock; use PHPUnit\Framework\TestCase; /** @@ -14,22 +14,25 @@ */ class MagentoTestCase extends TestCase { + /** + * @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(): 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/ObjectHandlerUtil.php b/dev/tests/unit/Util/ObjectHandlerUtil.php deleted file mode 100644 index 0e4543f2b..000000000 --- a/dev/tests/unit/Util/ObjectHandlerUtil.php +++ /dev/null @@ -1,145 +0,0 @@ -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]); - } - - /** - * Set up everything required to mock DataObjectHandler::getInstance() with $data value - * - * @param array $data - */ - public static function mockDataObjectHandlerWithData($data) - { - // Clear DataObjectHandler singleton if already set - $property = new \ReflectionProperty(DataObjectHandler::class, "INSTANCE"); - $property->setAccessible(true); - $property->setValue(null); - - $mockDataProfileSchemaParser = AspectMock::double(DataProfileSchemaParser::class, [ - 'readDataProfiles' => $data - ])->make(); - - $mockObjectManager = AspectMock::double(ObjectManager::class, [ - 'create' => $mockDataProfileSchemaParser - ])->make(); - - AspectMock::double(ObjectManagerFactory::class, [ - 'getObjectManager' => $mockObjectManager - ]); - } - - /** - * Set up everything required to mock PageObjectHandler::getInstance() with $data value - * - * @param array $data - */ - public static function mockPageObjectHandlerWithData($data) - { - // clear section object handler value to inject parsed content - $property = new \ReflectionProperty(PageObjectHandler::class, 'INSTANCE'); - $property->setAccessible(true); - $property->setValue(null); - - $mockSectionParser = AspectMock::double(PageParser::class, ["getData" => $data])->make(); - $instance = AspectMock::double(ObjectManager::class, ['get' => $mockSectionParser])->make(); - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); - } - - /** - * Set up everything required to mock SectionObjectHandler::getInstance() with $data value - * - * @param array $data - */ - public static function mockSectionObjectHandlerWithData($data) - { - // clear section object handler value to inject parsed content - $property = new \ReflectionProperty(SectionObjectHandler::class, "INSTANCE"); - $property->setAccessible(true); - $property->setValue(null); - - $mockSectionParser = AspectMock::double(SectionParser::class, ["getData" => $data])->make(); - $instance = AspectMock::double(ObjectManager::class, ["get" => $mockSectionParser])->make(); - AspectMock::double(ObjectManagerFactory::class, ["getObjectManager" => $instance]); - } - - /** - * Set up everything required to mock TestObjectHandler::getInstance() with $data value - * - * @param array $data - * @throws \Exception - */ - public static function mockTestObjectHandlerWitData($data) - { - // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); - $property->setAccessible(true); - $property->setValue(null); - - $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]); - } - - /** - * Set up everything required to mock ActionGroupObjectHandler::getInstance() with $data value - * - * @param array $data - * @throws \Exception - */ - public static function mockActionGroupObjectHandlerWithData($data) - { - // Clear action group object handler value to inject parsed content - $property = new \ReflectionProperty(ActionGroupObjectHandler::class, 'instance'); - $property->setAccessible(true); - $property->setValue(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]); - } -} 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 e40d80e83..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; diff --git a/dev/tests/unit/Util/TestLoggingUtil.php b/dev/tests/unit/Util/TestLoggingUtil.php index 1b0bce1f9..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,7 +97,16 @@ 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 @@ -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/MftfTestCase.php b/dev/tests/util/MftfTestCase.php index a563186b3..365a8351f 100644 --- a/dev/tests/util/MftfTestCase.php +++ b/dev/tests/util/MftfTestCase.php @@ -9,8 +9,10 @@ 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 { @@ -112,23 +114,19 @@ protected function assertExceptionRegex(string $expectClass, array $expectedMess 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 - $property = new \ReflectionProperty(SuiteGenerator::class, 'instance'); - $property->setAccessible(true); - $property->setValue(null); + $reflectionClass = new ReflectionClass(SuiteGenerator::class); + $reflectionClass->setStaticPropertyValue('instance', null); // clear suite object handler to force recollection of test data - $property = new \ReflectionProperty(SuiteObjectHandler::class, 'instance'); - $property->setAccessible(true); - $property->setValue(null); + $reflectionClass = new ReflectionClass(SuiteObjectHandler::class); + $reflectionClass->setStaticPropertyValue('instance', null); } } 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 index 08e0b44f8..fdf05ad4b 100644 --- a/dev/tests/verification/Resources/ActionGroupReturningValueTest.txt +++ b/dev/tests/verification/Resources/ActionGroupReturningValueTest.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ 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]'); } /** @@ -37,10 +44,15 @@ class ActionGroupReturningValueTestCest */ 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 ActionGroupReturningValueTestCest * @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 ActionGroupReturningValueTestCest $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 7d8fae946..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"); @@ -46,13 +61,19 @@ class ActionGroupWithStepKeyReferencesCest $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 index d4e1b6eb3..c986b6fb4 100644 --- a/dev/tests/verification/Resources/ActionsInDifferentModulesSuite.txt +++ b/dev/tests/verification/Resources/ActionsInDifferentModulesSuite.txt @@ -3,6 +3,7 @@ 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; @@ -11,6 +12,7 @@ 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. @@ -51,6 +53,8 @@ class ActionsInDifferentModulesSuite extends \Codeception\GroupObject private function executePreConditions() { if ($this->currentTestRun == 1) { + $this->testCount = $this->getTestCount(); + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); try { @@ -118,7 +122,7 @@ class ActionsInDifferentModulesSuite 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 )); @@ -126,7 +130,7 @@ class ActionsInDifferentModulesSuite 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"); @@ -194,8 +198,16 @@ class ActionsInDifferentModulesSuite extends \Codeception\GroupObject ); $availableSessions = RemoteWebDriver::getAllSessions($wdHost); foreach ($availableSessions as $session) { - $remoteWebDriver = RemoteWebDriver::createBySessionID($session['id'], $wdHost); - $remoteWebDriver->quit(); + 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 + } } } } @@ -215,4 +227,28 @@ class ActionsInDifferentModulesSuite extends \Codeception\GroupObject } 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 c8435675b..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 @@ -160,4 +177,10 @@ class AssertTestCest $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 29cfd9baf..5016fba93 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 @@ -116,6 +127,7 @@ class BasicFunctionalTestCest $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 @@ -140,7 +152,7 @@ class BasicFunctionalTestCest $I->moveForward(); // stepKey: moveForwardKey1 $I->moveMouseOver(".functionalTestSelector"); // stepKey: moveMouseOverKey1 $I->openNewTab(); // stepKey: openNewTabKey1 - $I->pause(); // stepKey: pauseKey1 + $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 @@ -158,6 +170,7 @@ class BasicFunctionalTestCest $I->seeElementInDOM(".functionalTestSelector"); // stepKey: seeElementInDOMKey1 $I->seeInCurrentUrl("/functionalUrl"); // stepKey: seeInCurrentUrlKey1 $I->seeInField(".functionalTestSelector", "someInput"); // stepKey: seeInFieldKey1 + $I->seeInSecretField(".functionalTestSelector", $I->getSecret("someKey")); // stepKey: seeInFieldKey2 $I->seeInPageSource("Home Page"); // stepKey: seeInPageSourceKey1 $I->seeInPageSource("

"); // stepKey: seeInPageSourceKey2 $I->seeInPopup("someInput"); // stepKey: seeInPopupKey1 @@ -185,4 +198,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("#`~!@#$%^&*()-_=+{}[]|\;:\".,>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 index 2f26a6463..4c19fea5e 100644 --- a/dev/tests/verification/Resources/ExtendedActionGroupReturningValueTest.txt +++ b/dev/tests/verification/Resources/ExtendedActionGroupReturningValueTest.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ 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"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -37,4 +52,10 @@ class ExtendedActionGroupReturningValueTestCest $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 index d043a5c04..a25ba031d 100644 --- a/dev/tests/verification/Resources/ExtendedChildActionGroupReturningValueTest.txt +++ b/dev/tests/verification/Resources/ExtendedChildActionGroupReturningValueTest.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ 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"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -40,4 +55,10 @@ class ExtendedChildActionGroupReturningValueTestCest $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 0202f6d4f..522ba7620 100644 --- a/dev/tests/verification/Resources/ExtendingSkippedTest.txt +++ b/dev/tests/verification/Resources/ExtendingSkippedTest.txt @@ -19,17 +19,22 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ExtendingSkippedTestCest { + /** + * @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, \Codeception\Scenario $scenario) { + 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 index 89884df82..034307e37 100644 --- a/dev/tests/verification/Resources/GroupSkipGenerationTest.txt +++ b/dev/tests/verification/Resources/GroupSkipGenerationTest.txt @@ -19,11 +19,26 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ 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"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -31,4 +46,10 @@ class GroupSkipGenerationTestCest 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 @@ +Test files

verification/TestModule/Test/BasicFunctionalTest/MagentoCliTest.xml
") + */ +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 index a4689779b..68fec4cfa 100644 --- a/dev/tests/verification/Resources/MergedActionGroupReturningValueTest.txt +++ b/dev/tests/verification/Resources/MergedActionGroupReturningValueTest.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ 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]'); } /** @@ -37,10 +44,15 @@ class MergedActionGroupReturningValueTestCest */ 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 MergedActionGroupReturningValueTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -74,4 +85,10 @@ class MergedActionGroupReturningValueTestCest $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 6f75ee096..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 @@ -39,4 +54,10 @@ class PageReplacementTestCest $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/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.txt b/dev/tests/verification/Resources/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.txt new file mode 100644 index 000000000..f35d7ca74 --- /dev/null +++ b/dev/tests/verification/Resources/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.txt @@ -0,0 +1,39 @@ +Test filesverification/TestModule/Test/SkippedTest/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.xml
") + */ +class SkippedTestWithIssueMustGetSkippedWithoutErrorExitCodeCest +{ + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @Stories({"skipped"}) + * @Severity(level = SeverityLevel::MINOR) + * @Features({"TestModule"}) + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode(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/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 ' /"); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey $I->comment("< > & \$abc \" abc ' /"); + $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 ' /"); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey $I->comment("< > & \$abc \" abc ' /"); + $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(""); $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 3156a59dd..946bf456e 100644 --- a/dev/tests/verification/Resources/functionalSuiteHooks.txt +++ b/dev/tests/verification/Resources/functionalSuiteHooks.txt @@ -3,6 +3,7 @@ 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; @@ -11,6 +12,7 @@ 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. @@ -51,6 +53,8 @@ class functionalSuiteHooks extends \Codeception\GroupObject private function executePreConditions() { if ($this->currentTestRun == 1) { + $this->testCount = $this->getTestCount(); + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); try { @@ -120,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 )); @@ -128,7 +132,7 @@ 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"); @@ -177,8 +181,16 @@ class functionalSuiteHooks extends \Codeception\GroupObject ); $availableSessions = RemoteWebDriver::getAllSessions($wdHost); foreach ($availableSessions as $session) { - $remoteWebDriver = RemoteWebDriver::createBySessionID($session['id'], $wdHost); - $remoteWebDriver->quit(); + 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 + } } } } @@ -198,4 +210,28 @@ class functionalSuiteHooks extends \Codeception\GroupObject } 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 02b50f092..a5a130345 100644 --- a/dev/tests/verification/Resources/functionalSuiteWithComments.txt +++ b/dev/tests/verification/Resources/functionalSuiteWithComments.txt @@ -3,6 +3,7 @@ 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; @@ -11,6 +12,7 @@ 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. @@ -51,6 +53,8 @@ class functionalSuiteWithComments extends \Codeception\GroupObject private function executePreConditions() { if ($this->currentTestRun == 1) { + $this->testCount = $this->getTestCount(); + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); try { @@ -104,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 )); @@ -112,7 +116,7 @@ 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"); @@ -157,8 +161,16 @@ class functionalSuiteWithComments extends \Codeception\GroupObject ); $availableSessions = RemoteWebDriver::getAllSessions($wdHost); foreach ($availableSessions as $session) { - $remoteWebDriver = RemoteWebDriver::createBySessionID($session['id'], $wdHost); - $remoteWebDriver->quit(); + 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 + } } } } @@ -178,4 +190,28 @@ class functionalSuiteWithComments extends \Codeception\GroupObject } 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/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 @@ - - + diff --git a/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionActionGroupWithStepKeyReferencesActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionActionGroupWithStepKeyReferencesActionGroup.xml index 516c2895b..a9275c23b 100644 --- a/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionActionGroupWithStepKeyReferencesActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionActionGroupWithStepKeyReferencesActionGroup.xml @@ -8,7 +8,7 @@ - + @@ -22,12 +22,12 @@ - + 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 @@ - + 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 @@ - + {{arg1}} - + {{arg2}} - + {{arg3}} diff --git a/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml index 6eefdaf51..14cbd836f 100644 --- a/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml @@ -90,6 +90,8 @@ + + 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 @@ + + + + + + test + bar + + 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 @@ + + + + + + test + + 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 @@ + + + + + + Qty_1000 + test + bar + + 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 @@ + + + + + + + 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 @@ + + + + + + + diff --git a/dev/tests/verification/TestModule/Test/BasicFunctionalTest/BasicFunctionalTest.xml b/dev/tests/verification/TestModule/Test/BasicFunctionalTest/BasicFunctionalTest.xml index 53bd1f7ef..e0196f3ab 100644 --- a/dev/tests/verification/TestModule/Test/BasicFunctionalTest/BasicFunctionalTest.xml +++ b/dev/tests/verification/TestModule/Test/BasicFunctionalTest/BasicFunctionalTest.xml @@ -72,6 +72,7 @@ + @@ -107,6 +108,7 @@ + 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 @@ + + + + + + + + + + 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"> - - - + - +
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"> - - - - 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 @@ Mac - {{simpleData.lastname}} - - - - $$createData1.firstname$$ + Bar - - 1 - Kovacs - Takeshi Kovacs - - - - - - 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 @@ + + + + + + + + <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 b24bac3b5..01d7bc88c 100644 --- a/dev/tests/verification/TestModule/Test/XmlDuplicateTest/XmlDuplicateTest.xml +++ b/dev/tests/verification/TestModule/Test/XmlDuplicateTest/XmlDuplicateTest.xml @@ -93,6 +93,8 @@ <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"/> @@ -305,6 +307,8 @@ <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"/> @@ -516,6 +520,8 @@ <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"/> 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/ResilientGenerationTest.php b/dev/tests/verification/Tests/ResilientGenerationTest.php index bf65094e3..9bb2b9479 100644 --- a/dev/tests/verification/Tests/ResilientGenerationTest.php +++ b/dev/tests/verification/Tests/ResilientGenerationTest.php @@ -76,19 +76,19 @@ public function setUp(): void $property = new \ReflectionProperty(SuiteGenerator::class, "instance"); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); $property = new \ReflectionProperty(DirSetupUtil::class, "DIR_CONTEXT"); $property->setAccessible(true); - $property->setValue([]); + $property->setValue(null, []); $property = new \ReflectionProperty(SuiteObjectHandler::class, "instance"); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); $property = new \ReflectionProperty(TestObjectHandler::class, "testObjectHandler"); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); } /** @@ -123,7 +123,7 @@ public function testGenerateAllSuites() SuiteGenerator::getInstance()->generateAllSuites($testManifest); foreach (SuiteTestReferences::$data as $groupName => $expectedContents) { - if (substr($groupName, 0, 11) != 'NotGenerate') { + 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']); @@ -175,7 +175,7 @@ function () use ($groupName) { ); } - if (substr($groupName, 0, 11) != 'NotGenerate') { + 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']); @@ -199,7 +199,7 @@ function () use ($groupName) { } // Validate log message - if (substr($groupName, 0, 11) != 'NotGenerate' + if (substr($groupName, 0, 11) !== 'NotGenerate' && !in_array($groupName, array_keys(self::$exceptionGrpLogs))) { $type = 'info'; $message = '/suite generated/'; @@ -261,19 +261,19 @@ public function tearDown(): void $property = new \ReflectionProperty(SuiteGenerator::class, "instance"); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); $property = new \ReflectionProperty(DirSetupUtil::class, "DIR_CONTEXT"); $property->setAccessible(true); - $property->setValue([]); + $property->setValue(null, []); $property = new \ReflectionProperty(SuiteObjectHandler::class, "instance"); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); $property = new \ReflectionProperty(TestObjectHandler::class, "testObjectHandler"); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); } /** diff --git a/dev/tests/verification/Tests/SchemaValidationTest.php b/dev/tests/verification/Tests/SchemaValidationTest.php index c6187974c..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(): 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 2bc73986a..b2d1c8fdc 100644 --- a/dev/tests/verification/Tests/SkippedGenerationTest.php +++ b/dev/tests/verification/Tests/SkippedGenerationTest.php @@ -41,4 +41,15 @@ public function testMultipleSkippedIssuesGeneration() { $this->generateAndCompareTest('SkippedTestTwoIssues'); } + + /** + * Tests skipped test doesnt fail to generate when there is issue in test + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testSkippedTestMustNotFailToGenerateWithErrorWhenThereIsIssueWithAnyOfTheStepsAsTheTestIsSkipped() + { + $this->generateAndCompareTest('SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode'); + } } diff --git a/dev/tests/verification/Tests/StaticCheck/DeprecationStaticCheckTest.php b/dev/tests/verification/Tests/StaticCheck/DeprecationStaticCheckTest.php index f6aef65f6..6b034adb1 100644 --- a/dev/tests/verification/Tests/StaticCheck/DeprecationStaticCheckTest.php +++ b/dev/tests/verification/Tests/StaticCheck/DeprecationStaticCheckTest.php @@ -5,11 +5,12 @@ */ namespace tests\verification\Tests; -use AspectMock\Test as AspectMock; 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 { @@ -33,8 +34,8 @@ public function testDeprecatedEntityUsageCheck() $staticCheck = new DeprecatedEntityUsageCheck(); $input = $this->mockInputInterface(self::TEST_MODULE_PATH); - AspectMock::double(StaticChecksList::class, ['getErrorFilesPath' => self::STATIC_RESULTS_DIR]); - + $reflectionClass = new ReflectionClass(StaticChecksList::class); + $reflectionClass->setStaticPropertyValue('errorFilesPath', self::STATIC_RESULTS_DIR); /** @var InputInterface $input */ $staticCheck->execute($input); @@ -47,4 +48,13 @@ public function testDeprecatedEntityUsageCheck() 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 index 7c8f77c2f..70ab480c4 100644 --- a/dev/tests/verification/Tests/StaticCheck/PauseActionStaticCheckTest.php +++ b/dev/tests/verification/Tests/StaticCheck/PauseActionStaticCheckTest.php @@ -5,12 +5,14 @@ */ namespace tests\verification\Tests; -use AspectMock\Test as AspectMock; +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 { @@ -27,14 +29,15 @@ class PauseActionStaticCheckTest extends MftfStaticTestCase /** * test static-check PauseActionUsageCheck. * - * @throws \Exception + * @throws Exception */ public function testPauseActionUsageCheck() { $staticCheck = new PauseActionUsageCheck(); $input = $this->mockInputInterface(self::TEST_MODULE_PATH); - AspectMock::double(StaticChecksList::class, ['getErrorFilesPath' => self::STATIC_RESULTS_DIR]); + $reflectionClass = new ReflectionClass(StaticChecksList::class); + $reflectionClass->setStaticPropertyValue('errorFilesPath', self::STATIC_RESULTS_DIR); /** @var InputInterface $input */ $staticCheck->execute($input); @@ -48,4 +51,13 @@ public function testPauseActionUsageCheck() 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 85fbbacc9..ea25048b4 100644 --- a/dev/tests/verification/Tests/SuiteGenerationTest.php +++ b/dev/tests/verification/Tests/SuiteGenerationTest.php @@ -493,7 +493,7 @@ public function tearDown(): void $property = new \ReflectionProperty(DirSetupUtil::class, "DIR_CONTEXT"); $property->setAccessible(true); - $property->setValue([]); + $property->setValue(null, []); } /** 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/backward-incompatible-changes.md b/docs/backward-incompatible-changes.md deleted file mode 100644 index 5ddf1c9cb..000000000 --- a/docs/backward-incompatible-changes.md +++ /dev/null @@ -1,163 +0,0 @@ -# MFTF 3.0.0 backward incompatible changes - -This page highlights backward incompatible changes between releases that have a major impact and require detailed explanation and special instructions to ensure third-party tests continue working with Magento core tests. - -## Minimum supported PHP version changes - -We changed the minimum PHP version requirement from 7.0 to 7.3. Because of the PHP version requirement change, this MFTF version only supports Magento 2.4 or later. - -## Folder structure changes - -We removed support to read test modules from the deprecated path `dev/tests/acceptance/tests/functional/Magento/FunctionalTest`. If there are test modules in this path, they should be moved to `dev/tests/acceptance/tests/functional/Magento`. - -## XSD schema changes - -- Files under test modules `ActionGroup`, `Page`, `Section`, `Test` and `Suite` only support a single entity per file. -- The `file` attribute from `<module>` has been removed from the suite schema. `<module file=""/>` is no longer supported in suites. -- Metadata filename format changed to ***`*Meta.xml`***. -- Only nested assertion syntax will be supported. See the [assertions page](test/assertions.md) for details. Here is an example of the nested assertion syntax: - ```xml - <assertEquals stepKey="assertAddressOrderPage"> - <actualResult type="const">$billingAddressOrderPage</actualResult> - <expectedResult type="const">$shippingAddressOrderPage</expectedResult> - </assertEquals> - ``` - -### Upgrading tests to the new schema - -The following table lists the upgrade scripts that are available to upgrade tests to the new schema. - -| Script name | Description | -|-----------------------|-----------------------------------------------------------------------------------------------------------| -|`splitMultipleEntitiesFiles`| Splits files that have multiple entities into multiple files with one entity per file. | -|`upgradeAssertionSchema`| Updates assert actions that uses the old assertion syntax into the new nested syntax.| -|`renameMetadataFiles`| Renames Metadata filenames to `*Meta.xml`.| -|`removeModuleFileInSuiteFiles`| Removes occurrences of `<module file=""/>` from all `<suite>`s.| -|`removeUnusedArguments`| Removes unused arguments from action groups.| -|`upgradeTestSchema`| Replaces relative schema paths to URN in test files.| - -To run the upgrade tests: - -1. Run `bin/mftf reset --hard` to remove old generated configurations. -1. Run `bin/mftf build:project` to generate new configurations. -1. Run `bin/mftf upgrade:tests`. [See command page for details](commands/mftf.md#upgradetests). -1. 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. - -## MFTF commands - -`--debug` option `NONE` removed for strict schema validation. Ensure there are no schema validation errors in test modules before running MFTF commands. - -## MFTF actions - -### `executeInSelenium` and `performOn` removed - -**Action**: Deprecated actions `executeInSelenium` and `performOn` are removed in favor of new action `helper`. - -**Reason**: `executeInSelenium` and `performOn` allowed custom PHP code to be written inline inside of XML files which was difficult to maintain, troubleshoot, and modify. - -**Details**: - -The `helper` allows test writers to solve advanced requirements beyond what MFTF offers out of the box. See [custom-helpers](custom-helpers.md) for more information on usage. - -Here is an example of using `helper` in place of `executeSelenium` to achieve same workflow. - -Old usage: - -```xml -<executeInSelenium function="function ($webdriver) use ($I) { - $heading = $webdriver->findElement(\Facebook\WebDriver\WebDriverBy::xpath('//div[contains(@class, \'inline-wysiwyg\')]//h2')); - $actions = new \Facebook\WebDriver\Interactions\WebDriverActions($webdriver); - $actions->moveToElement($heading, {{TinyMCEPartialHeadingSelection.startX}}, {{TinyMCEPartialHeadingSelection.startY}}) - ->clickAndHold() - ->moveToElement($heading, {{TinyMCEPartialHeadingSelection.endX}}, {{TinyMCEPartialHeadingSelection.endY}}) - ->release() - ->perform(); - }" stepKey="selectHeadingTextInTinyMCE"/> -``` - -New usage: - -```xml -<helper class="\Magento\PageBuilder\Test\Mftf\Helper\SelectText" method="selectText" stepKey="selectHeadingTextInTinyMCE"> - <argument name="context">//div[contains(@class, 'inline-wysiwyg')]//h2</argument> - <argument name="startX">{{TinyMCEPartialHeadingSelection.startX}}</argument> - <argument name="startY">{{TinyMCEPartialHeadingSelection.startY}}</argument> - <argument name="endX">{{TinyMCEPartialHeadingSelection.endX}}</argument> - <argument name="endY">{{TinyMCEPartialHeadingSelection.endY}}</argument> -</helper> -``` - -### `pauseExecution` removed - -**Action**: `pauseExecution` is removed in favor of `pause`. - -**Reason**: `[WebDriver]pauseExecution` is removed in Codeception 3 in favor of `I->pause()`. - -**Details**: - -See the [actions page for details](test/actions.md#pause). Here is a usage example: - -```xml -<pause stepKey="pauseExecutionKey"/> -``` - -### Removed assert actions - -**Action**: Assert actions `assertInternalType`, `assertNotInternalType` and `assertArraySubset` are removed. - -**Reason**: PHPUnit 9 has dropped support for these assertions. - -### Updated assert actions - -**Action**: The `delta` attribute has been removed from `assertEquals` and `assertNotEquals`. Instead, new assert actions have been introduced: - - - `assertEqualsWithDelta` - - `assertNotEqualsWithDelta` - - `assertEqualsCanonicalizing` - - `assertNotEqualsCanonicalizing` - - `assertEqualsIgnoringCase` - - `assertNotEqualsIgnoringCase` - -**Reason**: PHPUnit 9 has dropped support for optional parameters for `assertEquals` and `assertNotEquals` and has introduced these new assertions. - -**Details**: - -Usage of `assertEquals` or `assertNotEquals` with a specified `delta`, should be replaced with appropriate assertion from the above list. - -### `assertContains` supports only iterable haystacks - -**Action**: `assertContains` and `assertNotContains` now only supports iterable haystacks. These assert actions have been added to work with string haystacks: - -- `assertStringContainsString` -- `assertStringNotContainsString` -- `assertStringContainsStringIgnoringCase` -- `assertStringNotContainsStringIgnoringCase` - -**Reason**: With PHPUnit 9, `assertContains` and `assertNotContains` only allows iterable haystacks. New assertions have been introduced to support string haystacks. - -**Details**: - -Usages of `assertContains` and `assertNotContains` with string haystacks should be replaced with appropriate assertion from the above list. - -Usage example for string haystacks: - -```xml -<assertStringContainsString stepKey="assertDiscountOnPrice2"> - <actualResult type="const">$grabSimpleProdPrice2</actualResult> - <expectedResult type="string">$110.70</expectedResult> -</assertStringContainsString> -``` - -### `formatMoney` removed - -**Action**: `formatMoney` has been removed in favor of `formatCurrency`. - -**Reason**: PHP 7.4 has deprecated use of `formatMoney`. - -**Details**: Format input to specified currency according to the locale specified. - -Usage example: - -```xml -<formatCurrency userInput="1234.56789000" locale="de_DE" currency="USD" stepKey="usdInDE"/> -``` diff --git a/docs/best-practices.md b/docs/best-practices.md deleted file mode 100644 index 1f3a25e02..000000000 --- a/docs/best-practices.md +++ /dev/null @@ -1,217 +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. - -## Focus on reusability - -### Use existing Tests and resources - -Magento offers more than **3000** acceptance tests, **2500** [Action group]s, **750** Page declarations with more than **1500** Section definitions. -It is very probable that behaviour you want to test already exists as a Test or Action Group. -Instead of writing everything by yourself - use `extends` attribute to refer to existing element and customize it. - -**Reusable Resources** - -{%raw%} - -* Tests (reusable with `<test extends="...">` argument) -* Action Group (reusable with including `<actionGroup ref="...">`, or extending `<actionGroup extends="...">`) -* Pages (reusable with reference `{{PageDefinition.url}}`) -* Sections (reusable with reference `{{SectionDefinition.elementDefinition}}`) -* Data Entities (reusable with reference `<createData entity="...">"` or extending `<entity extends="...">`) - -{%endraw%} - -<div class="bs-callout bs-callout-warning" markdown="1"> - -Avoid using resources that are marked as **Deprecated**. Usually there is a replacement provided for a deprecated resource. - -</div> - -### Extract repetitive Actions - -Instead of writing a few of Tests that perform mostly the same actions, you should thing about [Action group] that is a container for repetitive Actions. -If each run needs different data, use `<arguments>` to inject necessary information. - -We recommend to keep Action Groups having single responsibility, for example `AdminLoginActionGroup`, which expected outcome is being logged in as Administrator when [Action group] is executed. - -## Contribute - -Although the Magento Core team and Contributors join forces to cover most of the features with tests, it is impossible to have this done quickly. -If you've covered Magento Core feature with Functional Tests - you are more than welcome to contribute. - -## 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 is a short description of what the action group does. - -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 8e6d76c11..000000000 --- a/docs/commands/codeception.md +++ /dev/null @@ -1,93 +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 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 4.1.4. -</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) - -e, --ext=EXT Run with extension enabled (multiple values allowed) - --report Show output in compact style - --html[=HTML] Generate html with results [default: "report.html"] - --xml[=XML] Generate JUnit XML Log [default: "report.xml"] - --phpunit-xml[=PHPUNIT-XML] Generate PhpUnit XML Log [default: "phpunit-report.xml"] - --tap[=TAP] Generate Tap Log [default: "report.tap.log"] - --json[=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 - -d, --debug Show debug and scenario output - --bootstrap[=BOOTSTRAP] Execute custom PHP script before running tests. Path can be absolute or relative to current working directory [default: false] - --no-redirect Do not redirect to Composer-installed version in vendor/codeception - --coverage[=COVERAGE] Run with code coverage - --coverage-html[=COVERAGE-HTML] Generate CodeCoverage HTML report in path - --coverage-xml[=COVERAGE-XML] Generate CodeCoverage XML report in file - --coverage-text[=COVERAGE-TEXT] Generate CodeCoverage text report in file - --coverage-crap4j[=COVERAGE-CRAP4J] Generate CodeCoverage report in Crap4J XML format - --coverage-phpunit[=COVERAGE-PHPUNIT] Generate CodeCoverage PHPUnit report in path - --no-exit Don't finish with exit code - -g, --group=GROUP Groups of tests to be executed (multiple values allowed) - -s, --skip=SKIP Skip selected suites (multiple values allowed) - -x, --skip-group=SKIP-GROUP Skip selected groups (multiple values allowed) - --env=ENV Run tests in selected environments. (multiple values allowed) - -f, --fail-fast Stop after first failure - --no-rebuild Do not rebuild actor classes on start - --seed=SEED Define random seed for shuffle setting - --no-artifacts Don't report about artifacts - -h, --help Display this help message - -q, --quiet Do not output any message - -V, --version Display this application version - --ansi Force ANSI output - --no-ansi Disable ANSI output - -n, --no-interaction Do not ask any interactive question - -c, --config[=CONFIG] Use custom path for config - -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug -``` - -<!-- 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 6ed472d47..000000000 --- a/docs/commands/mftf.md +++ /dev/null @@ -1,632 +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 AdminLoginSuccessfulTest StorefrontPersistedCustomerLoginTest -``` - -### Generate test by test and suite name - -```bash -vendor/bin/mftf generate:tests WYSIWYGDisabledSuite:AdminCMSPageCreatePageTest -``` - -### 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 AdminLoginSuccessfulTest StorefrontPersistedCustomerLoginTest -r -``` - -This command cleans up the previously generated tests; generates and runs the `AdminLoginSuccessfulTest` and `StorefrontPersistedCustomerLoginTest` tests. - -### Generate and run particular test in a specific suite's context - -```bash -vendor/bin/mftf run:test WYSIWYGDisabledSuite:AdminCMSPageCreatePageTest -r -``` - -This command cleans up previously generated tests; generates and run `AdminCMSPageCreatePageTest` within the context of the `WYSIWYGDisabledSuite`. - -### 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][]. - -## Error tolerance during generation - -Starting from version 3.2.0, MFTF will not fail right away when encountering generation errors. -Instead, MFTF will generate as many tests and suites as it can, log errors to `mftf.log`, and exit with a non-zero generation status. - -Note: -- Not all errors are tolerable at generation. For example, schema validation errors, parser errors, and WebApi authentication errors will cause `hard` failures, with no tests or suites being generated. -- Error tolerance in generation is meant to help local test development and testing and is expected to be run locally. All generation errors must be fixed in order to use other framework functionality, pass static checks, and to deliver MFTF tests. - -## 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. <br/>Example: `generate:tests --config=parallel --time=15` <br/>Option `--time` will be the default and the __default value__ is `10` when neither `--time` nor `--groups` is specified. <br/>Example: `generate:tests --config=parallel`| -| `-g,--groups` | Set number of groups to be split into when `--config=parallel` is used. <br>Example: `generate:tests --config=parallel --groups=300` <br/>Options `--time` and `--groups` are mutually exclusive and only one should be used.| -| `--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 misc.xml>] -``` - -`misc.xml` is typically located at `<project root>/.idea/misc.xml`. - -#### 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/misc.xml -``` - -### `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/AdminLoginSuccessfulTestCest.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. -Behavior for determining what tests to run is as follows: - -* If test names are specified, only those tests are run. -* If no test names are specified, tests are run according to `staticRuleset.json`. -* If no `staticRuleset.json` is found, all tests are run. - -Static checks errors are written to *.txt files under TEST_BP/tests/_output/static-results/ - -#### Usage - -```bash -vendor/bin/mftf static-checks [<names>]... -``` - -#### Options - -| Option | Description | -|-----------------------|-----------------------------------------------------------------------------------------------------------| -| `-p, --path` | Path to a MFTF test module to run "deprecatedEntityUsage" and "pauseActionUsage" static check scripts. Option is ignored by other static check scripts. - -#### 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 deprecatedEntityUsage -``` - -```bash -vendor/bin/mftf static-checks pauseActionUsage -``` - -```bash -vendor/bin/mftf static-checks annotations -``` - -```bash -vendor/bin/mftf static-checks deprecatedEntityUsage -p path/to/mftf/test/module -``` - -```bash -vendor/bin/mftf static-checks pauseActionUsage -p path/to/mftf/test/module -``` - -```bash -vendor/bin/mftf static-checks testDependencies actionGroupArguments -``` - -#### Existing static checks - -| Argument | Description | -|-----------------------|-----------------------------------------------------------------------------------------------------------| -|`testDependencies` | Checks that test dependencies do not violate Magento module's composer dependencies.| -|`actionGroupArguments` | Checks that action groups do not have unused arguments.| -|`deprecatedEntityUsage`| Checks that deprecated test entities are not being referenced.| -|`annotations`| Checks various details of test annotations, such as missing annotations or duplicate annotations.| -|`pauseUsage`| Checks that pause action is not used in action groups, tests or suites.| - -#### Defining ruleset - -The `static-checks` command will look for a `staticRuleset.json` file under either: - -* `dev/tests/acceptance/staticRuleset.json`, if embedded with Magento2 -* `dev/staticRuleset.json`, if standalone - -This file works as the default configuration to easily allow for the integration of `static-checks` in a CI environment. -Currently, the ruleset only defines the tests to run. Here is an example of the expected format: - -```json -{ - "tests": [ - "actionGroupArguments", - "anotherTest" - ] -} -``` - -### `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/ -``` - -### `codecept:run` - -A MFTF wrapper command that invokes `vendor/bin/codecept run`. This command runs tests in functional suite. Tests must be generated before using this command. - -#### Usage - -See the [Run Command](https://codeception.com/docs/reference/Commands#Run). - -```bash -vendor/bin/mftf codecept:run [<suite|test>] --[<option(s)>] -``` - -#### Examples - -```bash -# Run all tests in functional suite -vendor/bin/mftf codecept:run functional -``` - -```bash -# Run all tests in functional suite with options -vendor/bin/mftf codecept:run functional --verbose --steps --debug -``` - -```bash -# Run one test -vendor/bin/mftf codecept:run functional Magento/_generated/default/AdminCreateCmsPageTestCest.php --debug -``` - -```bash -# Run all tests in default group -vendor/bin/mftf codecept:run functional --verbose --steps -g default -``` - -<div class="bs-callout-warning"> -<p> -Note: You may want to limit the usage of this Codeception command with arguments and options for "acceptance" only, since it is what's supported by MFTF. -When using this command, you should change "acceptance" to "functional" when referring to Codeception documentation. -</p> -</div> - -<!-- 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 4eb273824..de7fd5859 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -52,16 +52,6 @@ Example: MAGENTO_ADMIN_USERNAME=admin ``` -### MAGENTO_ADMIN_PASSWORD - -The password that tests will use to log in to the Magento Admin page. - -Example: - -```conf -MAGENTO_ADMIN_PASSWORD=1234reTyt%$7 -``` - ## Advanced configuration Depending on the environment you use, you may need to configure MFTF more precisely by setting additional configuration parameters. @@ -362,7 +352,7 @@ 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. +When pause is enabled, MFTF will generate pause() command in _failed() hook so that test will pause execution when failed. ```conf ENABLE_PAUSE=true @@ -408,6 +398,26 @@ Example: 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. @@ -423,4 +433,4 @@ MAGENTO_ADMIN_WEBAPI_TOKEN_LIFETIME=10800 [`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/configure-2fa.md b/docs/configure-2fa.md deleted file mode 100644 index 7b01913b9..000000000 --- a/docs/configure-2fa.md +++ /dev/null @@ -1,53 +0,0 @@ -# Configuring MFTF for Two-Factor Authentication (2FA) - -Using two-factor authentication (2FA) with MFTF is possible with some configurations settings in Magento. -In this document, we will use Google as the authentication provider. - -## Configure Magento {#config-magento-2fa} - -To prepare Magento for MFTF testing when 2FA is enabled, set the following configurations through the Magento CLI. - -First, select `Google Authenticator` as Magento's 2FA provider: - -```bash -bin/magento config:set twofactorauth/general/force_providers google -``` - -Now set the OTP window to `60` seconds: - -```bash -bin/magento config:set twofactorauth/google/otp_window 60 -``` - -Set a base32-encoded `secret` for `Google Authenticator` to generate a OTP for the default admin user that you set for `MAGENTO_ADMIN_USERNAME` in `.env`: - -```bash -bin/magento security:tfa:google:set-secret <MAGENTO_ADMIN_USERNAME> <OTP_SHARED_SECRET> -``` - -## Configure the MFTF {#config-mftf-2fa} - -Save the same base32-encoded `secret` in a MFTF credential storage, e.g. `.credentials` file, `HashiCorp Vault` or `AWS Secrets Manager`. -More details are [here](./credentials.md). - -The path of the `secret` should be: - -```conf -magento/tfa/OTP_SHARED_SECRET -``` - -## GetOTP {#getOTP} - -A one-time password (OTP) is required when an admin user logs into the Magento admin. -Use the action `getOTP` [Reference](./test/actions.md#getotp) to generate the code and use it for the `Authenticator code` text field in 2FA - Google Auth page. - -Note: -You will need to set the `secret` for any non-default admin users first, before using `getOTP`. For example: - -{%raw%} - -```xml -<magentoCLI command="security:tfa:google:set-secret admin2 {{_CREDS.magento/tfa/OTP_SHARED_SECRET}}" stepKey="setSecret"/> -``` - -{%endraw%} diff --git a/docs/credentials.md b/docs/credentials.md deleted file mode 100644 index 0c6decb41..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 MFTF supports three types of credential storage: - -- **.credentials file** -- **HashiCorp Vault** -- **AWS Secrets Manager** - -## Configure File Storage - -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 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. - -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 MFTF will rely on the persisted token in the token helper (usually the local filesystem) for future API requests. - -### Store secrets in vault - -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. MFTF will 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. -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 diff --git a/docs/custom-helpers.md b/docs/custom-helpers.md deleted file mode 100644 index bf1ebc572..000000000 --- a/docs/custom-helpers.md +++ /dev/null @@ -1,129 +0,0 @@ -# Custom Helpers - -<div class="bs-callout-warning"> -Due to complexity, you should only write new custom helpers as a last resort, after trying to implement your test using built-in actions. -</div> - -Custom Helpers allow test writers to write custom test actions to solve advanced requirements beyond what MFTF offers out of the box. - -In MFTF version 3.0.0, the following test actions were removed: - -* `<executeInSelenium>` -* `<performOn>` - -These actions were removed because they allowed custom PHP code to be written inline inside of XML files. This code was difficult to read. It had no proper syntax highlighting and no linting. It was difficult to maintain, troubleshoot, and modify. - -However, sometimes custom logic beyond what MFTF offers is necessary so we have provided an alternative solution: the `<helper>` action. - -## Example - -Custom helpers are implemented in PHP files that must be placed in this directory: - -```text -<ModuleName>/Test/Mftf/Helper -``` - -This custom helper selects text on the page with this approach: - -1. Move to a very specific X,Y starting position. -1. Click and hold the mouse button down. -1. Move to another specific X,Y position. -1. Release the mouse button. - -This functionality is used to select text on the page and cannot be accomplished using built-in test actions. - -### PHP File - -```php -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\PageBuilder\Test\Mftf\Helper; - -use Magento\FunctionalTestingFramework\Helper\Helper; - -/** - * Class SelectText provides an ability to select needed text. - */ -class SelectText extends Helper -{ - /** - * Select needed text. - * - * @param string $context - * @param int $startX - * @param int $startY - * @param int $endX - * @param int $endY - * @return void - */ - public function selectText(string $context, int $startX, int $startY, int $endX, int $endY) - { - try { - /** @var \Magento\FunctionalTestingFramework\Module\MagentoWebDriver $webDriver */ - $webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); - - $contextElement = $webDriver->webDriver->findElement(\Facebook\WebDriver\WebDriverBy::xpath($context)); - $actions = new \Facebook\WebDriver\Interactions\WebDriverActions($webDriver->webDriver); - $actions->moveToElement($contextElement, $startX, $startY) - ->clickAndHold() - ->moveToElement($contextElement, $endX, $endY) - ->release() - ->perform(); - } catch (\Exception $e) { - $this->fail($e->getMessage()); - } - } -} -``` - -### Notes about this PHP file - -The following details are important about the above file: - -1. The `namespace` must match the file location: `namespace Magento\PageBuilder\Test\Mftf\Helper;` -2. The class must `extends Helper` and have the corresponding `use` statement to match. -3. You may access the WebDriver object via `$this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver')`. -4. You may implement multiple related methods within the same class. -5. Specify the correct function argument types to match the type of values you want to pass in. In this case, we specified `string $context, int $startX, int $startY, int $endX, int $endY`. In the XML we will match these types. - -You should follow the same patterns in any custom helpers that you write yourself, but you may implement any logic or iteration that you need to solve for your use case. - -### Referencing in a test - -Once you have implemented something like the above PHP file, reference it in a test: - -```xml -<helper class="\Magento\PageBuilder\Test\Mftf\Helper\SelectText" method="selectText" stepKey="selectHeadingTextInTinyMCE"> - <argument name="context">//div[contains(@class, 'inline-wysiwyg')]//h2</argument> - <argument name="startX">{{TinyMCEPartialHeadingSelection.startX}}</argument> - <argument name="startY">{{TinyMCEPartialHeadingSelection.startY}}</argument> - <argument name="endX">{{TinyMCEPartialHeadingSelection.endX}}</argument> - <argument name="endY">{{TinyMCEPartialHeadingSelection.endY}}</argument> -</helper> -``` - -### Notes about the XML - -1. Specify an argument value for every argument that matches our PHP implementation. This allows us to pass other test data to the Custom Helper. -1. The `class` attribute matches the namespace specified in the PHP file. -1. Specify the method from the class via the `method` attribute. -1. If the function has a return value, it will be assigned to the `stepKey` variable. In this case, `$selectHeadingTextInTinyMCE` holds the return value. -1. The types of argument values must match the PHP implementation's expected types. - -## Key takeaways - -Custom helpers allow you to solve complex use cases such as conditional logic, iteration, or complex WebDriver usage. - -With access to the WebDriver object, you have a lot of flexibility available to you. See the [Codeception WebDriver](https://github.com/Codeception/module-webdriver/blob/master/src/Codeception/Module/WebDriver.php) for technical details and functionality available for use. - -A custom helper is written in a PHP file and then referenced in test XML, like other actions. - -You should only use these as a last resort after trying to implement your test using built-in actions. - -## References - -[Codeception WebDriver source code](https://github.com/Codeception/module-webdriver/blob/master/src/Codeception/Module/WebDriver.php) - Reference for using the WebDriver Object diff --git a/docs/data.md b/docs/data.md deleted file mode 100644 index 100b95317..000000000 --- a/docs/data.md +++ /dev/null @@ -1,345 +0,0 @@ -# Input testing data - -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 %} --> - -The MFTF `<data>` entities are stored in `<module_dir>/Test/Mftf/Data/`. - -## 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. - -The following is an example of the usage of `<data>` entity in the `Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml` test: - -```xml -<actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToAllCustomerPage"> - <argument name="menuUiId" value="{{AdminMenuCustomers.dataUiId}}"/> - <argument name="submenuUiId" value="{{AdminMenuCustomersAllCustomers.dataUiId}}"/> -</actionGroup> -``` - -In the above example: - -* `AdminMenuCustomers` is an entity name. -* `dataUiId` is a `<data>` key of the entity. - -### 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. - -The following is an example of the usage of `_ENV` in the `Magento/Braintree/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml` action group: - -```xml -<fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteRoleSection.current_pass}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> -``` - -### 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][]. - -The following is an example of the usage of `_CREDS` in the `Magento/Braintree/Test/Mftf/Data/BraintreeData.xml` data entity: - -```xml -<entity name="MerchantId" type="merchant_id"> - <data key="value">{{_CREDS.magento/braintree_enabled_fraud_merchant_id}}</data> -</entity> -``` - -## 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. - -The following is an example of the usage of the persistant data in `Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml` test: - -```xml -<actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> - <argument name="email" value="$$createCustomer.email$$"/> -</actionGroup> -``` - -<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> - -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 `stepKey` value can only be referenced within the test scope that it is defined in (`test`, `before/after`). - -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 the `<data>` entity 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 (`Magento/Catalog/Test/Mftf/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>`. Use camel case for entity names. -`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. - -Example: - -```xml -<data key="name" unique="suffix">simpleCategory</data> -``` - -### 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.* - -Example: - -```xml -<var key="parent_id" entityType="category" entityKey="id" /> -``` - -### 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. - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|optional|Key attribute of <item/> entity in which to assign a value. By default numeric key will be generated. - - -<!-- 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 576bfdb24..000000000 --- a/docs/extending.md +++ /dev/null @@ -1,342 +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> - -<div class="bs-callout-warning" markdown="1"> -<br> -Note: The extended test will be skipped if the parent test is skipped. -</div> - -## Extending tests - -### Update a test step - -<!-- {% raw %} --> - -__Use case__: Create two similar tests with a different action group reference by overwriting a `stepKey`. - -> Test with "extends": - -```xml -<tests> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> - <test name="AdminLoginAsOtherUserSuccessfulTest" extends="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginAsOtherUserActionGroup" stepKey="loginAsAdmin"/> - </test> -</tests> -``` - -> Test without "extends": - -```xml -<tests> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> - <test name="AdminLoginAsOtherUserSuccessfulTest"> - <actionGroup ref="AdminLoginAsOtherUserActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -### Add a test step - -__Use case__: Create two similar tests where the second test contains two additional steps specified to occur `before` or `after` other `stepKeys`. - -> Tests with "extends": - -```xml -<tests> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> - <test name="AdminLoginCheckRememberMeSuccessfulTest" extends="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminCheckRememberMeActionGroup" stepKey="checkRememberMe" after="loginAsAdmin"/> - <actionGroup ref="AssertAdminRememberMeActionGroup" stepKey="assertRememberMe" before="logoutFromAdmin"/> - </test> -</tests> -``` - -> Tests without "extends": - -```xml -<tests> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> - <test name="AdminLoginCheckRememberMeSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCheckRememberMeActionGroup" stepKey="checkRememberMe"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AssertAdminRememberMeActionGroup" stepKey="assertRememberMe"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -### Update a test before hook - -__Use case__: Create two similar tests where the second test contains an additional action in the `before` hook. - -> Tests with "extends": - -```xml -<tests> - <test name="AdminLoginSuccessfulTest"> - <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - </before> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> - <test name="AdminLoginCheckRememberMeSuccessfulTest" extends="AdminLoginSuccessfulTest"> - <before> - <actionGroup ref="AdminCheckRememberMeActionGroup" stepKey="checkRememberMe" after="loginAsAdmin"/> - </before> - </test> -</tests> -``` - -> Tests without "extends": - -```xml -<tests> - <test name="AdminLoginSuccessfulTest"> - <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - </before> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> - <test name="AdminLoginCheckRememberMeSuccessfulTest"> - <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCheckRememberMeActionGroup" stepKey="checkRememberMe"/> - </before> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -## Extending action groups - -Extend an [action group] to add or update [actions] in your module. - -### Update an action - -__Use case__: The `AssertAdminCountProductActionGroup` action group counts the particular product. -Modify the action group to use another product. - -> Action groups with "extends": - -```xml -<actionGroups> - <actionGroup name="AssertAdminCountProductActionGroup"> - <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="AssertAdminOtherCountProductActionGroup" extends="AssertAdminCountProductActionGroup"> - <grabMultiple selector="selectorForProductB" stepKey="grabProducts"/> - </actionGroup> -</actionGroups> -``` - -> Action groups without "extends": - -```xml -<actionGroups> - <actionGroup name="AssertAdminCountProductActionGroup"> - <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="AssertAdminOtherCountProductActionGroup"> - <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 `AdminGetProductCountActionGroup` action group returns the count of products. -Add a new test `AssertAdminVerifyProductCountActionGroup` that asserts the count of products: - -> Action groups with "extends": - -```xml -<actionGroups> - <actionGroup name="AdminGetProductCountActionGroup"> - <arguments> - <argument name="productSelector" type="string"/> - </arguments> - <grabMultiple selector="{{productSelector}}" stepKey="grabProducts"/> - </actionGroup> - - <actionGroup name="AssertAdminVerifyProductCountActionGroup" extends="AdminGetProductCountActionGroup"> - <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="AdminGetProductCountActionGroup"> - <arguments> - <argument name="productSelector" type="string"/> - </arguments> - <grabMultiple selector="{{productSelector}}" stepKey="grabProducts"/> - </actionGroup> - - <actionGroup name="AssertAdminVerifyProductCountActionGroup"> - <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 ad6fa766a..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,368 +0,0 @@ -# Getting started - -<div class="bs-callout bs-callout-info" markdown="1"> -[Find your version] of MFTF. -The latest Magento 2.3.x release supports MFTF 2.6.4. -The latest Magento 2.2.x release supports MFTF 2.5.3. -</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.4-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 -``` - -### Testing with the Magento Two-Factor Authentication (2FA) extension {#2fa} - -If the Magento instance under test has the [Magento Two-Factor Authentication (2FA) extension][] installed and enabled, additional configurations is needed to run MFTF tests. Learn more in [Configure MFTF for Magento with Two-Factor Authentication (2FA)](./configure-2fa.md). - -### Webserver configuration {#web-server-configuration} - -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 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**. - -Or via command line: - -```bash -bin/magento config:set web/seo/use_rewrites 1 -``` - -You must clean the cache after changing the configuration values: - -```bash -bin/magento cache:clean config full_page -``` - -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 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 MFTF as a standalone tool, refer to [Set up a standalone MFTF][]. - -Install 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/misc.xml -``` - -If the file does not exist, add the `--force` option to create it: - -```bash -vendor/bin/mftf generate:urn-catalog --force .idea/misc.xml -``` - -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 Magento project root, run the following command to enable 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 the terminal. -For example, the following commands download and run the Selenium server for Google Chrome: - -```bash -curl -O http://selenium-release.storage.googleapis.com/3.14/selenium-server-standalone-3.14.0.jar -``` - -```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 -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 `AdminLoginSuccessfulTest`, run: - -```bash -vendor/bin/mftf run:test AdminLoginSuccessfulTest --remove -``` - -See more commands in [`mftf`][]. - -### Step 5. Generate reports {#reports} - -During testing, MFTF generates test reports in CLI. You can generate visual representations of the report data using the [Allure Framework][]. To view the reports in a 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 - -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. -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 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 - -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 AdminLoginSuccessfulTest -``` - -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.4/install-gde/system-requirements.html -[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 version]: introduction.html#find-your-mftf-version -[Installation Guide docroot]: https://devdocs.magento.com/guides/v2.4/install-gde/tutorials/change-docroot-to-pub.html -[Magento Two-Factor Authentication (2FA) extension]: https://devdocs.magento.com/guides/v2.4/security/two-factor-authentication.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/cicd.md b/docs/guides/cicd.md deleted file mode 100644 index 80cf134d8..000000000 --- a/docs/guides/cicd.md +++ /dev/null @@ -1,114 +0,0 @@ -# How to use MFTF in CICD - -To integrate MFTF tests into your CICD pipeline, it is best to start with the conceptual flow of the pipeline code. - -## Concept - -The overall workflow that tests should follow is: - -- Obtain a Magento instance + install pre-requisites. -- Generate the tests. - - Set options for single or parallel running. -- Delegate and run tests and gather test-run artifacts. - - Re-run options. -- Generate the Allure reports from the results. - -## Obtain a Magento instance - -To start, we need a Magento instance to operate against for test generation and execution. - -```bash -git clone https://github.com/magento/magento2 -``` - -or - -```bash -composer create-project --repository=https://repo.magento.com/ magento/project-community-edition magento2ce -``` - -For more information on installing magento see [Install Magento using Composer][]. - -After installing the Magento instance, set a couple of configurations to the Magento instance: - -```bash -bin/magento config:set general/locale/timezone America/Los_Angeles -bin/magento config:set admin/security/admin_account_sharing 1 -bin/magento config:set admin/security/use_form_key 0 -bin/magento config:set cms/wysiwyg/enabled disabled -``` - -These set the default state of the Magento instance. If you wish to change the default state of the application (and have updated your tests sufficiently to account for it), this is the step to do it. - -If your magento instance has Two-Factor Authentication enabled, see [Configure 2FA][] to configure MFTF tests. - -## Install Allure - -This is required for generating the report after your test runs. See [Allure][] for details. - -## Generate tests - -### Single execution - -Generate tests based on what you want to run: - -```bash -vendor/bin/mftf generate:tests -``` - -This will generate all tests and a single manifest file under `dev/tests/acceptance/tests/functional/Magento/_generated/testManifest.txt`. - -### Parallel execution - -To generate all tests for use in parallel nodes: - -```bash -vendor/bin/mftf generate:tests --config parallel -``` - -This generates a folder under `dev/tests/acceptance/tests/functional/Magento/_generated/groups`. This folder contains several `group#.txt` files that can be used later with the `mftf run:manifest` command. - -## Delegate and run tests - -### Single execution - -If you are running on a single node, call: - -```bash -vendor/bin/mftf run:manifest dev/tests/acceptance/tests/functional/Magento/_generated/testManifest.txt -``` - -### Parallel execution - -You can optimize your pipeline by running tests in parallel across multiple nodes. - -Tests can be split up into roughly equal running groups using `--config parallel`. - -You do not want to perform installations on each node again and build it. So, to save time, stash pre-made artifacts from earlier steps and un-stash on the nodes. - -The groups can be then distributed on each of the nodes and run separately in an isolated environment. - -- Stash artifacts from main node and un-stash on current node. -- Run `vendor/bin/mftf run:manifest <current_group.txt>` on current node. -- Gather artifacts from `dev/tests/acceptance/tests/_output` from current node to main node. - -### Rerun options - -In either single or parallel execution, to re-run failed tests, simply add the `run:failed` command after executing a manifest: - -```bash -vendor/bin/mftf run:failed -``` - -### Generate Allure report - -In the main node, generate reports using your `<path_to_results>` into a desired output path: - -```bash -allure generate <path_to_results> -c -o <path_to_output> -``` - -<!-- Link definitions --> -[Install Magento using Composer]: https://devdocs.magento.com/guides/v2.4/install-gde/composer.html -[Configure 2FA]: ../configure-2fa.md -[Allure]: https://docs.qameta.io/allure/ 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-extending-precedence.png b/docs/img/mftf-extending-precedence.png deleted file mode 100644 index deed5231f..000000000 Binary files a/docs/img/mftf-extending-precedence.png and /dev/null differ 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/interactive-pause.md b/docs/interactive-pause.md deleted file mode 100644 index 250f19936..000000000 --- a/docs/interactive-pause.md +++ /dev/null @@ -1,71 +0,0 @@ -# Interactive Pause - -It can be difficut to write a successful test on the first attempt. You will need to try different commands, with different arguments, before you find the correct path. - -Since Codeception 3.0, you can pause execution in any point and enter an interactive shell where you will be able to try commands in action. - -Now this `Interactive Pause` feature is available in MFTF. All you need to do is to set `ENABLE_PAUSE` to `true` in `.env`. - -Check [pause on codeception.com][] for documentation and a video to see `Interactive Pause` in action. - -In short, when a test gets to `$I->pause()` step, it stops and shows a console where you can try all available commands with auto-completion, stash commands, save screenshots, etc. - -## MFTF Run Commands - -The following MFTF run commands support `Interactive Pause` when `ENABLE_PAUSE` is set to `true`. - -```bash -vendor/bin/mftf run:group -``` - -```bash -vendor/bin/mftf run:test -``` - -```bash -vendor/bin/mftf run:manifest -``` - -```bash -vendor/bin/mftf run:failed -``` - -### Use `Interactive Pause` During Test Development - -Here is a typical work flow for this use case: - -- Set `ENABLE_PAUSE` to `true` under `.env` -- Add <pause> action in a test where you want to pause execution for debugging -- Run test -- Execution should pause at <pause> action and invoke interactive console -- Try out commands in interactive console -- Resume test execution by pressing `ENTER` - -### Use `Pause` On Test Failure - -When `ENABLE_PAUSE` is set to `true`, MFTF automatically generates `pause()` action in `_failed()` hook for tests and in `_failed()` function in `MagentoWebDriver`. -This allows you to use `pause` to debug test failure for a long running test. The work flow might look like: - -- Set `ENABLE_PAUSE` to `true` under `.env` -- Run test -- Execution pauses and invokes interactive console right after test fails -- Examine and debug on the spot of failure - -## MFTF Codecept Run Command - -You can also use MFTF's wrapper command to run Codeception directly and activate `Interactive Pause` by passing `--debug` option. -You do not need to set `ENABLE_PAUSE` to `true` for this command if you don't want to pause on test failure. - -```bash -vendor/bin/mftf codecept:run --debug -``` - -<div class="bs-callout-warning"> -<p> -Note: MFTF command "--debug" option has different meaning than Codeception command "--debug" mode option. -</p> -</div> - -<!-- Link definitions --> - -[pause on codeception.com]: https://codeception.com/docs/02-GettingStarted#Interactive-Pause diff --git a/docs/introduction.md b/docs/introduction.md deleted file mode 100644 index 73b068ba0..000000000 --- a/docs/introduction.md +++ /dev/null @@ -1,163 +0,0 @@ -# Introduction to the Magento Functional Testing Framework - -<div class="bs-callout bs-callout-info" markdown="1"> -This documentation is for MFTF 3.0, which was release in conjunction with Magento 2.4. -MFTF 3.0 is a major update and introduces many new changes and fixes. -MFTF 2 docs can be found [here][]. -</div> - -[Find your version] of MFTF. - -The Magento Functional Testing Framework (MFTF) is a framework used to perform automated end-to-end functional testing. - -## Goals - -- To facilitate functional testing and minimize the effort it takes to perform regression testing. -- Enable extension developers to provide functional tests for their extensions. -- Ensure a common standard of quality between Magento, extension developers and system integrators. - -MFTF also focuses on - -- **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. - -## Audience - -- **Contributors**: Tests build confidence about the results of changes introduced to the platform. -- **Extension Developers**: Can adjust expected behaviour according to their customizations. -- **System Integrators**: MFTF coverage provided out-of-the-box with Magento is solid base for Acceptance / Regression Tests. - -## MFTF tests - -MFTF supports two different locations for storing the tests and test artifacts: - -- `<magento_root>/app/code/<vendor_name>/<module_name>/Test/Mftf/` is the location of local, customized tests. -- `<magento_root>/vendor/<vendor_name>/<module_name>/Test/Mftf/` is location of tests provided by Magento and vendors. - -If you installed Magento with Composer, please refer to `vendor/magento/<module_dir>/Test/Mftf/` for examples. - -### Directory Structure - -The file structure under both cases is the same: - -```tree -Test -└── Mftf - ├── ActionGroup - │   └── ... - ├── Data - │   └── ... - ├── Metadata - │   └── ... - ├── Page - │   └── ... - ├── Section - │   └── ... - └── Test - └── ... -``` - -## Use cases - -- Contributor: changes the core behaviour, fixing the annoying bug. - He wants to have automated "supervisor" which is going to verify his work continuously across the stages of bug fixing. Finally, when fix is done - Functional Test is also proof of work done. -- Extension Developer: offers extension that changes core behaviour. - He can easily write new tests to make sure that after enabling the feature, Magento behaves properly. Everything with just extending existing tests. As a result he don't need to write coverage from scratch. -- Integration Agency: maintains Client's e-commerce. - They are able to customize tests delivered with Magento core to follow customizations implemented to Magento. After each upgrade they can just run the MFTF tests to know that no regression was introduced. - -## 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 - -## Find your MFTF version - -There are two options to find out your MFTF version: - -- using the MFTF CLI -- using the Composer CLI - -All the Command Line commands needs to be executed from `<magento_root>` - -### MFTF CLI - -```bash -vendor/bin/mftf --version -``` - -### Composer CLI - -```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 - -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 -[MFTF project]: https://github.com/magento/magento2-functional-testing-framework -[Find your version]: #find-your-mftf-version -[here]: ../v2/docs/introduction.html diff --git a/docs/merge_points/extend-action-groups.md b/docs/merge_points/extend-action-groups.md deleted file mode 100644 index 6e7a54d70..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="AdminFillSimpleProductFormActionGroup"> - <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> -``` - -## Extend file - -```xml -<actionGroup name="AdminFillSimpleProductFormWithMyExtensionActionGroup" extends="AdminFillSimpleProductFormActionGroup"> - <!-- 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="AdminFillSimpleProductFormActionGroup"> - <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="AdminFillSimpleProductFormWithMyExtensionActionGroup"> - <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 91d2a67bb..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> -``` - -## Extend file - -```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 763977049..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="AdminLoginActionGroup" stepKey="AdminLoginActionGroup1"/> - <actionGroup ref="AdminFillSimpleProductFormActionGroup" 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> -``` - -## Extend file - -```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="AdminLoginActionGroup" stepKey="AdminLoginActionGroup1"/> - <actionGroup ref="AdminFillSimpleProductFormActionGroup" 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="AdminLoginActionGroup" stepKey="AdminLoginActionGroup1"/> - <actionGroup ref="AdminFillSimpleProductFormActionGroup" 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 ca6d20e68..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 can 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="AdminFillSimpleProductFormActionGroup"> - <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="AdminFillSimpleProductFormActionGroup"> - <!-- 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="AdminFillSimpleProductFormActionGroup"> - <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 135b3f7d8..000000000 --- a/docs/merge_points/merge-sections.md +++ /dev/null @@ -1,46 +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="AdminProductsPageSection"> - <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="AdminProductsPageSection"> - <!-- myExtensionElement will simply be added to the page --> - <element name="myExtensionElement" type="button" selector="input.myExtension"/> -</section> -``` - -## Resultant section - -```xml -<section name="AdminProductsPageSection"> - <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> -``` - -<!-- {% 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 b9e50d296..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="AdminLoginActionGroup" stepKey="adminLoginActionGroup1"/> - <actionGroup ref="AdminFillSimpleProductFormActionGroup" 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="AdminLoginActionGroup" stepKey="AdminLoginActionGroup1"/> - <actionGroup ref="AdminFillSimpleProductFormActionGroup" 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 d8436492e..000000000 --- a/docs/merging.md +++ /dev/null @@ -1,569 +0,0 @@ -# Merging - -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="SampleSection">` 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="SelectNotLoggedInCustomerGroupActionGroup">` 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. - -## Merging precedence - -**Magento Functional Testing Framework** uses Module's `<sequence>` to merge all XML configurations into Codeception instructions. If there's no Sequence specified, MFTF would use: - -1. Vendor modules (Magento & Vendors) located in `vendor/` -1. Tests located in `app/code/*/*/Test/Mftf` - -![Usual precedence for merging MFTF Tests and resources][mftfExtendingPrecedence image] - -## Add a test - -You cannot add another [`<test>`][tests] using merging functionality. -To add a `<test>`, create a new `*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 use `skip` annotation to skip the test. - -Learn more about running tests with different options using [`mftf`] or [`codecept`] commands. - -### Example - -Skip the `AdminLoginSuccessfulTest` test in the `.../Backend/Test/AdminLoginSuccessfulTest.xml` file while merging with the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -<!-- {% raw %} --> - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest"> - <annotations> - <features value="Backend"/> - <stories value="Login on the Admin Login page"/> - <title value="Admin should be able to log into the Magento Admin backend successfully"/> - <description value="Admin should be able to log into the Magento Admin backend successfully"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-71572"/> - <group value="example"/> - <group value="login"/> - </annotations> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest"> - <annotations> - <skip> - <issueId value="Issue#"/> - </skip> - </annotations> - </test> -</tests> -``` - -The `AdminLoginSuccessfulTest` result corresponds to: - -```xml -<test name="AdminLoginSuccessfulTest"> - <annotations> - <features value="Backend"/> - <stories value="Login on the Admin Login page"/> - <title value="Admin should be able to log into the Magento Admin backend successfully"/> - <description value="Admin should be able to log into the Magento Admin backend successfully"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-71572"/> - <group value="example"/> - <group value="login"/> - <skip> - <issueId value="Issue#"/> - </skip> - </annotations> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> -</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 `AdminCheckOptionSalesActionGroup` before `AssertAdminSuccessLoginActionGroup` (`stepKey="assertLoggedIn"`) and add `AdminAssertSalesOnDashboardActionGroup` after the `AssertAdminSuccessLoginActionGroup` in the `AdminLoginSuccessfulTest` test (in the `.../Backend/Test/AdminLoginSuccessfulTest.xml` file) while merging with the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminCheckOptionSalesActionGroup" stepKey="checkOptionSales" before="assertLoggedIn"/> - <actionGroup ref="AdminAssertSalesOnDashboardActionGroup" stepKey="assertSalesOnDashboard" after="assertLoggedIn"/> - </test> -</tests> -``` - -The `AdminLoginSuccessfulTest` result corresponds to: - -```xml -<test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCheckOptionSalesActionGroup" stepKey="checkOptionSales" before="assertLoggedIn"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminAssertSalesOnDashboardActionGroup" stepKey="assertSalesOnDashboard" after="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> -</test> -``` - -### Remove a test step - -**Use case**: Remove `AdminCheckOptionSalesActionGroup` (`stepKey="checkOptionSales"`) from the `LogInAsAdminTest` test (in the `.../Backend/Test/AdminLoginSuccessfulTest.xml` file) while merging with the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCheckOptionSalesActionGroup" stepKey="checkOptionSales"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest"> - <remove keyForRemoval="checkOptionSales"/> - </test> -</tests> -``` - -The `AdminLoginSuccessfulTest` result corresponds to: - -```xml -<test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> -</test> -``` - -### Update a test step - -**Use case**: Change argument in `AdminCheckOptionSalesActionGroup` (`stepKey="checkOptionSales"`) of the `AdminLoginSuccessfulTest` test (in the `.../Backend/Test/AdminLoginSuccessfulTest.xml` file) while merging with the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCheckOptionSalesActionGroup" stepKey="checkOptionSales"> - <argument name="salesOption" value="{{AdminSalesData.lifeTimeSales}}"/> - </actionGroup> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -```xml -<test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCheckOptionSalesActionGroup" stepKey="checkOptionSales"> - <argument name="salesOption" value="{{AdminSalesData.annualSales}}"/> - </actionGroup> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> -</test> -``` - -The `AdminLoginSuccessfulTest` result corresponds to: - -```xml -<test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCheckOptionSalesActionGroup" stepKey="checkOptionSales"> - <argument name="salesOption" value="{{AdminSalesData.annualSales}}"/> - </actionGroup> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> -</test> -``` - -### Add several test steps {#insert-after} - -**Use case**: Add several action groups after the `AdminLoginActionGroup` (`stepKey="loginAsAdmin"`) in the `AdminLoginSuccessfulTest` test (in the `.../Backend/Test/AdminLoginSuccessfulTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> -</test> -``` - -Create the `.../Foo/Test/AdminLoginSuccessfulTest.xml` file: - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest" insertAfter="loginAsAdmin"> - <actionGroup ref="AdminCheckOptionSalesActionGroup" stepKey="checkOptionSales"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -The `AdminLoginSuccessfulTest` result corresponds to: - -```xml -<tests ...> - <test name="AdminLoginSuccessfulTest"> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCheckOptionSalesActionGroup" stepKey="checkOptionSales"/> - <actionGroup ref="AssertAdminSuccessLoginActionGroup" stepKey="assertLoggedIn"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </test> -</tests> -``` - -## 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="SelectNotLoggedInCustomerGroupActionGroup"> - <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> -</actionGroup> -``` - -> B2B Merge file - -```xml -<!-- name matches --> -<actionGroup name="SelectNotLoggedInCustomerGroupActionGroup"> - <!-- 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 `AdminBaseBackendSection` and `AdminAnotherBackendSection` to the `AdminBaseBackendPage` (`.../Backend/Page/AdminBaseBackendPage.xml` file): - -```xml -<pages ...> - <page name="AdminBaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="AdminBaseBackendSection"/> - <section name="AdminAnotherBackendSection"/> - </page> -</pages> -``` - -Create the `.../FooBackend/Page/AdminBaseBackendPage.xml` file: - -```xml -<pages ...> - <page name="AdminBaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="AdminNewExtensionSection"/> - </page> -</pages> -``` - -The `AdminBaseBackendPage` result corresponds to: - -```xml -<page name="AdminBaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - - <section name="AdminBaseBackendSection"/> - <section name="AdminAnotherBackendSection"/> - <section name="AdminNewExtensionSection"/> -</page> -``` - -### Remove a section - -**Use case**: The `FooBackend` module extends the `Backend` module and requires deletion of the `AdminAnotherBackendSection` section (the `.../Backend/Page/AdminBaseBackendPage.xml` file): - -```xml -<page name="AdminBaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="AdminBaseBackendSection"/> - <section name="AdminAnotherBackendSection"/> -</page> -``` - -Create the `.../FooBackend/Page/AdminBaseBackendPage.xml` file: - -```xml -<page name="AdminBaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="AdminAnotherBackendSection" remove="true"/> -</page> -``` - -The `AdminBaseBackendPage` result corresponds to: - -```xml -<page name="AdminBaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="AdminBaseBackendSection"/> -</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 -[mftfExtendingPrecedence image]: img/mftf-extending-precedence.png diff --git a/docs/metadata.md b/docs/metadata.md deleted file mode 100644 index 414f4bc04..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 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, 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 - -MFTF allows you to handle basic CRUD operations with an object using [Magento REST API][api reference] requests. -To convert a request to 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"/> -``` - -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 MFTF to search an operation with `dataType="category"`. -Since the action is __to create__ a category, 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"/> -``` - -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, 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"/> -``` - -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} - -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.4/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.4/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 fd03cfd58..000000000 --- a/docs/page.md +++ /dev/null @@ -1,174 +0,0 @@ -# Page structure - -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="urn:magento:mftf: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="urn:magento:mftf: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 a 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 only one `<page>` element. - -### 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 629c3cfd8..000000000 --- a/docs/reporting.md +++ /dev/null @@ -1,313 +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 -Generate Tests Command Run -Codeception PHP Testing Framework v4.1.4 -Powered by PHPUnit 9.1.3 by Sebastian Bergmann and contributors. -Running with seed: - -Magento\FunctionalTestingFramework.functional Tests (2) ------------------------ -Modules: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver, \Magento\FunctionalTestingFramework\Module\MagentoSequence, \Magento\FunctionalTestingFramework\Module\MagentoAssert, \Magento\FunctionalTestingFramework\Module\MagentoActionProxies, Asserts, \Magento\FunctionalTestingFramework\Helper\HelperContainer -``` - -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 `4.1.4` that uses `PHPUnit` of version `9.1.3`. - -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\Module\MagentoSequence, \Magento\FunctionalTestingFramework\Module\MagentoAssert, \Magento\FunctionalTestingFramework\Module\MagentoActionProxies, Asserts, \Magento\FunctionalTestingFramework\Helper\HelperContainer,..` - -#### Passed tests - -The next chunk of the log reports about test execution of the first test: - -```terminal -AdminLoginSuccessfulTestCest: Admin login successful test -Signature: Magento\AcceptanceTest\_default\Backend\AdminLoginSuccessfulTestCest:AdminLoginSuccessfulTest -Test: tests/functional/Magento/_generated/default/AdminLoginSuccessfulTestCest.php:AdminLoginSuccessfulTest -Scenario -- -[loginAsAdmin] AdminLoginActionGroup - [navigateToAdmin] am on page "/admin/admin" - [fillUsername] fill field "#username","admin" - [fillPassword] fill field "#login","123123q" - [clickLogin] click ".actions .action-primary" - [clickLoginWaitForPageLoad] wait for page load 30 - [clickDontAllowButtonIfVisible] conditional click ".modal-popup .action-secondary",".modal-popup .action-secondary",true - [closeAdminNotification] close admin notification -[assertLoggedIn] AssertAdminSuccessLoginActionGroup - [waitForAdminAccountTextVisible] wait for element visible ".page-header .admin-user-account-text",60 - [assertAdminAccountTextElement] see element ".page-header .admin-user-account-text" -[logoutFromAdmin] AdminLogoutActionGroup - [amOnLogoutPage] am on page "/admin/admin/auth/logout/" - PASSED -``` - -The running test is `AdminLoginSuccessfulTestCest`, which is `Admin login successful test` (this text is generated from the test name but with the `Cest` part excluded). -Its test signature is `Magento\AcceptanceTest\_default\Backend\AdminLoginSuccessfulTestCest:AdminLoginSuccessfulTest` that matches a `className:methodName` format using namespaces. -A path to the corresponding `Test` is `tests/functional/Magento/_generated/default/AdminLoginSuccessfulTestCest.php:AdminLoginSuccessfulTest` (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/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:AdminMenuNavigationWithSecretKeysTest -Scenario -- -[enableUrlSecretKeys] magento cli "config:set admin/security/use_form_key 1",60 -Value was saved. - -[cleanInvalidatedCaches1] magento cli "cache:clean config full_page",60 -Cleaned cache types: -config -full_page - -[loginAsAdmin] AdminLoginActionGroup - [navigateToAdmin] am on page "/admin/admin" - [fillUsername] fill field "#username","admin" - [fillPassword] fill field "#login","123123q" - [clickLogin] click ".actions .action-primary" - [clickLoginWaitForPageLoad] wait for page load 30 - [clickDontAllowButtonIfVisible] conditional click ".modal-popup .action-secondary",".modal-popup .action-secondary",true - [closeAdminNotification] close admin notification -[clickStoresMenuOption1] click "#menu-magento-backend-stores" -[waitForStoresMenu1] wait for loading mask to disappear -[clickStoresConfigurationMenuOption1] click "#nav li[data-ui-id='menu-magento-config-system-config']" -[waitForConfigurationPageLoad1] wait for page load 60 -[seeCurrentUrlMatchesConfigPath1] see current url matches "~\/admin\/system_config\/~" -[clickCatalogMenuOption] click "#something" -[saveScreenshot] save screenshot -[disableUrlSecretKeys] magento cli "config:set admin/security/use_form_key 0",60 -Value was saved. - -[cleanInvalidatedCaches2] magento cli "cache:clean config full_page",60 -Cleaned cache types: -config -full_page - -[logout] AdminLogoutActionGroup - [amOnPage] am on page "/admin/admin/auth/logout/" - FAIL --------------------------------------------------------------------------------- -``` - -The general test details and scenario has the same format as in the Passed test. - -```terminal -[clickCatalogMenuOption] click "#something" -[saveScreenshot] save screenshot -``` - -When a test step fails, MFTF always saves a screenshot of the web page with the failing state immediately after the failure occurs. -`[saveScreenshot] save screenshot` follows the failing test step `[clickCatalogMenuOption] click "#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 `saveScreenshot` 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 -Time: 02:07.534, Memory: 150.50 MB - -There was 1 failure: ---------- -``` -MFTF reports that the test run took 02:07.534 using 150.50 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/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:AdminMenuNavigationWithSecretKeysTest - Step Click "#something" - Fail CSS or XPath element with '#something' was not found. - -Scenario Steps: - - 27. // Exiting Action Group [logout] AdminLogoutActionGroup - 26. $I->amOnPage("/admin/admin/auth/logout/") at tests/functional/Magento/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:55 - 25. // Entering Action Group [logout] AdminLogoutActionGroup - 24. // Cleaned cache types: -config -full_page - - 23. $I->magentoCLI("cache:clean config full_page",60) at tests/functional/Magento/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:52 - 22. // Value was saved. -``` - -- `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/_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 Click "#something"` - the failing test step is the *click* action with the *#something* selector. It would correspond the `<click selector="#something" ... />` test step in the XML defined tests. - -Finally, the report finishes with fairly self-descriptive lines. - -```terminal -FAILURES! -Tests: 2, Assertions: 2, Failures: 1. -``` - -MFTF encountered failures due to the last test run, that included *2* tests with *2* 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 68a6507af..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: - -> StorefrontCustomerSignInPopupFormSection.xml - -```xml -... -<element name="signIn" type="button" selector="#send2" timeout="30"/> -... -``` - -The test step that covers the use case: - -> CaptchaWithDisabledGuestCheckoutTest.xml - -```xml -... -<click selector="{{StorefrontCustomerSignInPopupFormSection.signIn}}" stepKey="clickSignIn"/> -... -``` - -<!-- {% 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 d2dc9924c..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="LocatorFunctionTest"> - <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 9f623f276..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/_generated/`. -All tests that are not within a suite are generated in the _default_ suite at `<magento 2 root>/dev/tests/acceptance/tests/functional/Magento/_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"/>`. - - 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. -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="urn:magento:mftf: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="urn:magento:mftf: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 4a77016f1..000000000 --- a/docs/test.md +++ /dev/null @@ -1,142 +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 individual test [`<test>`] with its metadata ([`<annotations>`]), before ([`<before>`]) and after ([`<after>`]) section. - -MFTF `<test>` 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 a test XML file 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: - -* One `<test>` tag is allowed per test XML file. -* All names within the framework are in the **PascalCase** format and must be alphanumeric. -* Each action and action group call should have its own identifier `<stepKey>`. -* A test may have any number of [assertions][assertion] at any point within the `<test>`. -* If `<test>` is included in [`<suite>`][suites], it **cannot be generated in isolation** from `<before>` and `<after>` section of the suite (see [suites] for details). - -## Elements reference - -There are several XML elements that are used within `<test>` in the MFTF. - -### tests {#tests-tag} - -`<tests>` is a container for test and must contain exactly one [`<test>`]. - -### test {#test-tag} - -`<test>` is a set of steps, including [actions], [assertions][assertion] and Action Group calls. 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 that are preconditions for the [`<test>`]. For example: Change configuration, create Customer Account, Create Category and Product. - -`<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. The goal of this section is to perform cleanup (revert the environment to the pre-test state). - -`<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 05adf795e..000000000 --- a/docs/test/action-groups.md +++ /dev/null @@ -1,306 +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> -``` - -## Return a value - -Action groups can return a value using a `return` tag. - -```xml -<actionGroup name="GetOrderIdActionGroup"> - <seeElement selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="assertOrderLink"/> - <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="orderId"/> - <return value="{$orderId}" stepKey="returnOrderId"/> -</actionGroup> -``` - -The value returned can be accessed in later steps using action group step key `{$getOrderId}`. -```xml -<actionGroup ref="GetOrderIdActionGroup" stepKey="getOrderId"/> -<!--Filter the Order using Order ID --> -<actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> - <argument name="orderId" value="{$getOrderId}"/> -</actionGroup> -``` -### Convention to return a value - -The following conventions apply to action groups returning a value: -- Only action groups can return value. Use of `return` tag is dis-allowed in tests and suites. -- An action group does not support multiple `return` tags. -- For [merging action groups](../merging.md#merge-action-groups), `return` is allowed only in one of the merging action groups. -- Value returned by an action group can only be referenced within the scope that the action group is defined in (`test`, `before/after`). - -## 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 42ecde053..000000000 --- a/docs/test/actions.md +++ /dev/null @@ -1,2505 +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) -* [getOTP](#getotp) -* [return](#return) - -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="{{AdminLogoutPage.url}}" 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"/> -``` - -### formatCurrency -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 - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|required| Number to be formatted. -`locale`|string|required| The locale to format to. -`currency`|string|required| The 3-letter ISO 4217 currency code indicating the currency to use. -`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. - -### getOTP - -Generate a one-time password (OTP) based on a saved `secret` at path `magento/tfa/OTP_SHARED_SECRET` in a MFTF credential storage. -The one-time password (OTP) is returned and accessible through the stepkey. - -MFTF use TOTP from [Spomky-Labs/otphp](https://github.com/Spomky-Labs/otphp), if you want to learn more about this action. - -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 -<getOTP stepKey="getOtp"/> -``` - -### 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"/> -``` - -### return - -Specifies what value is returned by an action group. The value can be then accessed in later steps using the action group stepKey. See [Action groups returning a value](./action-groups.md#return-a-value) for usage information. - -Attribute|Type|Use|Description ----|---|---|--- -`value`|string|required| value returned by action group. -`stepKey`|string|required| A unique identifier of the action. - -#### Example - -```xml -<!-- Returns value of $grabInputName to the calling --> -<return value="{$grabInputName}" stepKey="returnInputName"/> -``` - -### 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. - -### pause - -See usage of `<pause` in [interactive-pause](../interactive-pause.md) and [pause docs on codeception.com](https://codeception.com/docs/02-GettingStarted#Interactive-Pause). - -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. --> -<pause 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 79cc00e80..000000000 --- a/docs/test/annotations.md +++ /dev/null @@ -1,243 +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. - -#### 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 `<severity>` element is an implementation of the [`@Severity`] Allure annotation, which is used to prioritise tests by severity. - -Attribute|Type|Use|Acceptable values ----|---|---|--- -`value`|string|required|`MINOR`, `AVERAGE`, `MAJOR`, `CRITICAL`, `BLOCKER` - -#### Example - -```xml -<severity value="CRITICAL"/> -``` - -#### Usage guidelines - -Severity Level|Usage ----|--- -`BLOCKER`|If this test fails, the customer is completely blocked from purchasing a product. -`CRITICAL`|This is a serious problem impacting conversion, or affecting the operation of the store. -`MAJOR`|Store conversion rate is reduced owing to this issue. For example, something is broken or missing that impacts checkout frequency or cart volume. -`AVERAGE`|A fault on the storefront that can negatively impact conversion rate (like UI errors or omissions), or problems with Magento admin functionality. -`MINOR`|An application or configuration fault that has no impact on conversion rate. - -### 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://github.com/allure-framework/allure-phpunit#extended-test-class-or-test-method-description -[`@Features`]: https://github.com/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://github.com/allure-framework/allure-phpunit#set-test-severity -[`@Stories`]: https://github.com/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://github.com/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 a3e97cf51..000000000 --- a/docs/test/assertions.md +++ /dev/null @@ -1,704 +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"` - -In case of `assertContains` actions, `<expectedResult>` is the needle and `<actualResult>` is the haystack. - -## 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 - -The `<assertElementContainsAttribute>` asserts that the selected html element contains and matches the expected value for the given attribute. - -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](https://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](https://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. - -### assertContains - -See [assertContains docs on codeception.com](https://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. - -### assertStringContainsString - -See [assertStringContainsString docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertStringContainsString). - -Example: - -```xml -<assertStringContainsString stepKey="assertDropDownTierPriceTextProduct1"> - <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> - <actualResult type="variable">DropDownTierPriceTextProduct1</actualResult> -</assertStringContainsString> -``` - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text describing the cause of the failure. -`stepKey`|string|required| Unique identifier of the text step. -`before`|string|optional| `stepKey` of the action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertStringContainsStringIgnoringCase - -See [assertStringContainsStringIgnoringCase docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertStringContainsStringIgnoringCase). - -Example: - -```xml -<assertStringContainsStringIgnoringCase stepKey="verifyContentType"> - <actualResult type="variable">grabContentType</actualResult> - <expectedResult type="string">{{image.extension}}</expectedResult> -</assertStringContainsStringIgnoringCase> -``` - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Message describing the cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of the action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertCount - -See [assertCount docs on codeception.com](https://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](https://codeception.com/docs/modules/Asserts#assertEmpty). - -Example: - -```xml -<assertEmpty stepKey="assertSearchButtonEnabled"> - <actualResult type="string">$grabSearchButtonAttribute</actualResult> -</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](https://codeception.com/docs/modules/Asserts#assertEquals). - -Example: - -```xml -<assertEquals message="ExpectedPrice" stepKey="assertBundleProductPrice"> - <actualResult type="variable">grabProductPrice</actualResult> - <expectedResult type="string">$75.00</expectedResult> -</assertEquals> -``` - -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. - -### assertEqualsWithDelta - -See [assertEqualsWithDelta docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertEqualsWithDelta). - -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. - -### assertEqualsCanonicalizing - -See [assertEqualsCanonicalizing docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertEqualsCanonicalizing). - -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. - -### assertEqualsIgnoringCase - -See [assertEqualsIgnoringCase docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertEqualsIgnoringCase). - -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. - -### assertFalse - -See [assertFalse docs on codeception.com](https://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](https://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](https://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](https://codeception.com/docs/modules/Asserts#assertGreaterOrEquals). - -Example: - -```xml -<assertGreaterOrEquals stepKey="checkStatusSortOrderAsc" after="getOrderStatusSecondRow"> - <actualResult type="const">$getOrderStatusSecondRow</actualResult> - <expectedResult type="const">$getOrderStatusFirstRow</expectedResult> -</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](https://codeception.com/docs/modules/Asserts#assertGreaterThan). - -Example: - -```xml -<assertGreaterThan stepKey="checkQuantityWasChanged"> - <actualResult type="const">$grabEndQuantity</actualResult> - <expectedResult type="const">$grabStartQuantity</expectedResult> -</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](https://codeception.com/docs/modules/Asserts#assertGreaterThanOrEqual). - -Example: - -```xml -<assertGreaterThanOrEqual stepKey="checkStatusSortOrderAsc" after="getOrderStatusSecondRow"> - <actualResult type="const">$getOrderStatusSecondRow</actualResult> - <expectedResult type="const">$getOrderStatusFirstRow</expectedResult> -</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](https://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. - -### assertIsEmpty - -See [assertIsEmpty docs on codeception.com](https://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](https://codeception.com/docs/modules/Asserts#assertLessOrEquals). - -Example: - -```xml -<assertLessOrEquals stepKey="checkHeightIsCorrect"> - <actualResult type="variable">getImageHeight</actualResult> - <expectedResult type="variable">getSectionHeight</expectedResult> -</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](https://codeception.com/docs/modules/Asserts#assertLessThan). - -Example: - -```xml -<assertLessThan stepKey="assertLessImages"> - <expectedResult type="variable">initialImages</expectedResult> - <actualResult type="variable">newImages</actualResult> -</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](https://codeception.com/docs/modules/Asserts#assertLessThanOrEqual). - -Example: - -```xml -<assertLessThanOrEqual stepKey="checkHeightIsCorrect"> - <actualResult type="variable">getImageHeight</actualResult> - <expectedResult type="variable">getSectionHeight</expectedResult> -</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](https://codeception.com/docs/modules/Asserts#assertNotContains). - -Example: - -```xml -<assertNotContains stepKey="assertCustomerGroupNotInOptions"> - <actualResult type="variable">customerGroups</actualResult> - <expectedResult type="string">{{customerGroup.code}}</expectedResult> -</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. - -### assertStringNotContainsString - -See [assertStringNotContainsString docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertStringNotContainsString). - -Example: - -```xml -<assertStringNotContainsString stepKey="checkoutAsGuest"> - <expectedResult type="string">{{CaptchaData.checkoutAsGuest}}</expectedResult> - <actualResult type="variable">$formItems</actualResult> -</assertStringNotContainsString> -``` - -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. - -### assertStringContainsStringIgnoringCase - -See [assertStringNotContainsStringIgnoringCase docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertStringNotContainsStringIgnoringCase). - -Example: - -```xml -<assertStringContainsStringIgnoringCase stepKey="verifyContentType"> - <actualResult type="variable">grabContentType</actualResult> - <expectedResult type="string">{{image.extension}}</expectedResult> -</assertStringContainsStringIgnoringCase> -``` - -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](https://codeception.com/docs/modules/Asserts#assertNotEmpty). - -Example: - -```xml -<assertNotEmpty stepKey="checkSwatchFieldForAdmin"> - <actualResult type="const">$grabSwatchForAdmin</actualResult> -</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](https://codeception.com/docs/modules/Asserts#assertNotEquals). - -Example: - -```xml -<assertNotEquals stepKey="assertNotEquals"> - <actualResult type="string">{$grabTotalAfter}</actualResult> - <expectedResult type="string">{$grabTotalBefore}</expectedResult> -</assertNotEquals> -``` - -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. - -### assertNotEqualsWithDelta - -See [assertNotEqualsWithDelta docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertNotEqualsWithDelta). - -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. - -### assertNotEqualsCanonicalizing - -See [assertNotEqualsCanonicalizing docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertNotEqualsCanonicalizing). - -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. - -### assertNotEqualsIgnoringCase - -See [assertNotEqualsIgnoringCase docs on codeception.com](https://codeception.com/docs/modules/Asserts#assertNotEqualsIgnoringCase). - -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. - -### assertNotInstanceOf - -See [assertNotInstanceOf docs on codeception.com](https://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](https://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](https://codeception.com/docs/modules/Asserts#assertNotRegExp). - -Example: - -```xml -<assertNotRegExp stepKey="simpleThumbnailIsNotDefault"> - <actualResult type="const">$getSimpleProductThumbnail</actualResult> - <expectedResult type="const">'/placeholder\/thumbnail\.jpg/'</expectedResult> -</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](https://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](https://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](https://codeception.com/docs/modules/Asserts#assertRegExp). - -Example: - -```xml -<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> -``` - -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](https://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](https://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](https://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](https://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](https://codeception.com/docs/modules/Asserts#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](https://codeception.com/docs/modules/Asserts#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/update.md b/docs/update.md deleted file mode 100644 index 284730430..000000000 --- a/docs/update.md +++ /dev/null @@ -1,53 +0,0 @@ -# Update the Magento Functional Testing Framework - -<div class="bs-callout bs-callout-info" markdown="1"> -[Find your version] of MFTF. -The latest Magento 2.4.x release supports MFTF 3.x. -The latest Magento 2.3.x release supports MFTF 2.6.x. -</div> - -Tests and the Framework itself are stored in different repositories. - -* Tests are stored in Module's directory. -* MFTF is installed separately (usually as a Composer dependency) - -To understand different types of update - please follow the [Versioning][] page. - -## Patch version update - -Takes place when **third** digit of version number changes. - -1. Make sure that [Security settings][] are set appropriately. -1. Get latest Framework version with `composer update magento/magento2-functional-testing-framework --with-dependencies` -1. Generate updated tests with `vendor/bin/mftf generate:tests` - -## Minor version update - -Takes place when **second** digit of version number changes. - -1. Check details about backward incompatible changes in the [Changelog][] and update your new or customized tests. -1. Perform all the actions provided for [Patch Version Update][] -1. When updating from versions below `2.5.0`, verify [WYSIWYG settings][] -1. You may need to run the `upgrade:tests` using `vendor/bin/mftf upgrade:tests` - -## Major version update - -Takes place when **first** digit of version number changes. - -1. Check detailed explanation and instructions on major version upgrade in [Backward Incompatible Changes][] and upgrade your new or customized tests. -1. Perform all the actions provided for [Minor Version Update][] - -## After updating - -1. It is a good idea to regenerate your IDE Schema Definition catalog with `vendor/bin/mftf generate:urn-catalog .idea/misc.xml` -1. Update your tests, including data, metadata and other resources. Check if they contain tags that are unsupported in the newer version. -1. Remove the references to resources (ActionGroups, Sections, Tests) marked as deprecated. - -<!-- Link Definitions --> -[Changelog]: https://github.com/magento/magento2-functional-testing-framework/blob/master/CHANGELOG.md -[Backward Incompatible Changes]: backward-incompatible-changes.md -[WYSIWYG settings]: getting-started.md#wysiwyg-settings -[Security settings]: getting-started.md#security-settings -[Find your version]: introduction.md#find-your-mftf-version -[Versioning]: versioning.md#versioning-policy -[Patch Version Update]: #patch-version-update diff --git a/docs/versioning.md b/docs/versioning.md deleted file mode 100644 index 3cb51e405..000000000 --- a/docs/versioning.md +++ /dev/null @@ -1,71 +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 -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.4.0 | 3.0.0 | -| 2.3.5 | 2.6.4 | -| 2.3.4 | 2.5.3 | -| 2.3.3 | 2.4.5 | -| 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 25400bd63..fe6dd19a9 100644 --- a/etc/config/.credentials.example +++ b/etc/config/.credentials.example @@ -1,4 +1,5 @@ magento/tfa/OTP_SHARED_SECRET +magento/MAGENTO_ADMIN_PASSWORD #magento/carriers_fedex_account= #magento/carriers_fedex_meter_number= diff --git a/etc/config/.env.example b/etc/config/.env.example index 1c181cf8f..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 @@ -25,6 +24,8 @@ 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 @@ -58,7 +59,10 @@ MODULE_ALLOWLIST=Magento_Framework,ConfigurableProductWishlist,ConfigurableProdu #ALLOW_SKIPPED=true #*** Default timeout for wait actions -#WAIT_TIMEOUT=30 +WAIT_TIMEOUT=60 + +#*** 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 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 d5dbde466..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: @@ -24,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%" @@ -38,4 +38,4 @@ modules: capabilities: unhandledPromptBehavior: "ignore" chromeOptions: - args: ["--no-sandbox", "--window-size=1280,1024", "--disable-extensions", "--enable-automation", "--disable-gpu", "--enable-Passthrough", "--disable-dev-shm-usage", "--ignore-certificate-errors"] + 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 f5184808c..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|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|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|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"> + <!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"> diff --git a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php deleted file mode 100644 index 7d7a7bd44..000000000 --- a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php +++ /dev/null @@ -1,447 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -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; -use Yandex\Allure\Adapter\Event\StepFailedEvent; -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 - * - * Extends AllureAdapter to provide further information for allure reports - * - * @package Magento\FunctionalTestingFramework\Allure - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ - -class MagentoAllureAdapter extends AllureCodeception -{ - const STEP_PASSED = "passed"; - /** - * Test files cache. - * - * @var array - */ - private $testFiles = []; - - /** - * Boolean value to indicate if steps are invisible steps - * - * @var boolean - */ - private $atInvisibleSteps = false; - - /** - * Array of group values passed to test runner command - * - * @return string - */ - private function getGroup() - { - if ($this->options['groups'] != null) { - return $this->options['groups'][0]; - } - return null; - } - - /** - * Override of parent method to set suitename as suitename and group name concatenated - * - * @param SuiteEvent $suiteEvent - * @return void - */ - public function suiteBefore(SuiteEvent $suiteEvent) - { - $changeSuiteEvent = $suiteEvent; - - if ($this->getGroup() != null) { - $suite = $suiteEvent->getSuite(); - $suiteName = ($suite->getName()) . "\\" . $this->sanitizeGroupName($this->getGroup()); - - call_user_func(\Closure::bind( - function () use ($suite, $suiteName) { - $suite->name = $suiteName; - }, - null, - $suite - )); - - //change suiteEvent - $changeSuiteEvent = new SuiteEvent( - $suiteEvent->getSuite(), - $suiteEvent->getResult(), - $suiteEvent->getSettings() - ); - } - // call parent function - parent::suiteBefore($changeSuiteEvent); - } - - /** - * 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) - { - $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; - } - - /** - * Override of parent method: - * prevent replacing of . to • - * strips control characters - * inserts stepKey into step name - * - * @param StepEvent $stepEvent - * @return void - * @throws \Yandex\Allure\Adapter\AllureException - */ - public function stepBefore(StepEvent $stepEvent) - { - $stepAction = $stepEvent->getStep()->getAction(); - - // Set atInvisibleSteps flag and return if step is in INVISIBLE_STEP_ACTIONS - 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 (!($stepEvent->getStep() instanceof Comment)) { - $stepKey = $this->retrieveStepKey($stepEvent->getStep()->getLine()); - } - - // 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 - && !($stepEvent->getStep() instanceof Comment) - ) { - $stepAction = $stepEvent->getStep()->getHumanizedActionWithoutArguments(); - } - $stepArgs = $stepEvent->getStep()->getArgumentsAsString($argumentsLength); - - if (!trim($stepAction)) { - $stepAction = $stepEvent->getStep()->getMetaStep()->getHumanizedActionWithoutArguments(); - $stepArgs = $stepEvent->getStep()->getMetaStep()->getArgumentsAsString($argumentsLength); - } - - $stepName = ''; - if ($stepKey !== null) { - $stepName .= '[' . $stepKey . '] '; - } - $stepName .= $stepAction . ' ' . $stepArgs; - - // Strip control characters so that report generation does not fail - $stepName = preg_replace('/[[:cntrl:]]/', '', $stepName); - - $this->emptyStep = false; - $this->getLifecycle()->fire(new StepStartedEvent($stepName)); - } - - /** - * Override of parent method, fires StepFailedEvent if step has failed (for xml output) - * @param StepEvent $stepEvent - * @throws \Yandex\Allure\Adapter\AllureException - * @return void - */ - public function stepAfter(StepEvent $stepEvent = null) - { - // Simply return if step is INVISIBLE_STEP_ACTIONS - if ($this->atInvisibleSteps) { - if ($stepEvent->getStep()->hasFailed()) { - $this->atInvisibleSteps = false; - } - return; - } - - if ($stepEvent->getStep()->hasFailed()) { - $this->getLifecycle()->fire(new StepFailedEvent()); - } - - $this->getLifecycle()->fire(new StepFinishedEvent()); - } - - /** - * Override of parent method, fires a TestCaseFailedEvent if a test is marked as incomplete. - * - * @param FailEvent $failEvent - * @return void - */ - public function testIncomplete(FailEvent $failEvent) - { - $event = new TestCaseFailedEvent(); - $e = $failEvent->getFail(); - $message = $e->getMessage(); - $this->getLifecycle()->fire($event->withException($e)->withMessage($message)); - } - - /** - * Override of parent method. Adds in steps for hard PHP Errors if they arrise. - * - * @param FailEvent $failEvent - * @return void - */ - public function testError(FailEvent $failEvent) - { - $event = new TestCaseBrokenEvent(); - $e = $failEvent->getFail(); - $message = $e->getMessage(); - - // Create new step with an error for Allure - $failStep = new Step(); - $failStep->setName("ERROR"); - $failStep->setTitle($message); - $failStep->setStatus(Status::BROKEN); - - // Retrieve Allure Steps and add in the new BROKEN step - $rootStep = $this->getLifecycle()->getStepStorage()->pollLast(); - $rootStep->addStep($failStep); - $this->getLifecycle()->getStepStorage()->put($rootStep); - - $this->getLifecycle()->fire($event->withException($e)->withMessage($message)); - } - - /** - * 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(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 = []; - $actionGroupStepContainer = null; - - $actionGroupStepKey = null; - foreach ($rootStep->getSteps() as $step) { - $this->removeAttachments($step, $testFailed); - $stepKey = str_replace($actionGroupStepKey, '', $step->getName()); - if ($stepKey !== '[]' && $stepKey !== null) { - $step->setName($stepKey); - } - // if actionGroup flag, start nesting - if (strpos($step->getName(), ActionGroupObject::ACTION_GROUP_CONTEXT_START) !== false) { - if ($actionGroupStepContainer !== null) { - //actionGroup still being nested, need to close out and finish it. - $formattedSteps[] = $actionGroupStepContainer; - $actionGroupStepContainer = null; - $actionGroupStepKey = null; - } - - $step->setName(str_replace(ActionGroupObject::ACTION_GROUP_CONTEXT_START, '', $step->getName())); - $actionGroupStepContainer = $step; - $actionGroupStepKey = $this->retrieveActionGroupStepKey($step); - continue; - } - - // if actionGroup ended, add stack to steps - if (stripos($step->getName(), ActionGroupObject::ACTION_GROUP_CONTEXT_END) !== false) { - $formattedSteps[] = $actionGroupStepContainer; - $actionGroupStepContainer = null; - $actionGroupStepKey = null; - continue; - } - - if ($actionGroupStepContainer !== null) { - $actionGroupStepContainer->addStep($step); - if ($step->getStatus() !== self::STEP_PASSED) { - // If step didn't pass, need to end action group nesting and set overall step status - $actionGroupStepContainer->setStatus($step->getStatus()); - $formattedSteps[] = $actionGroupStepContainer; - $actionGroupStepContainer = null; - } - } else { - // Add step as normal - $formattedSteps[] = $step; - } - } - - // No public function for setting the step's steps - call_user_func(\Closure::bind( - function () use ($rootStep, $formattedSteps) { - $rootStep->steps = $formattedSteps; - }, - null, - $rootStep - )); - - $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. - * - * @param Step $step - * @return string|null - */ - private function retrieveActionGroupStepKey($step) - { - $actionGroupStepKey = null; - - preg_match(TestGenerator::ACTION_GROUP_STEP_KEY_REGEX, $step->getName(), $matches); - - if (!empty($matches['actionGroupStepKey'])) { - $actionGroupStepKey = ucfirst($matches['actionGroupStepKey']); - } - - return $actionGroupStepKey; - } - - /** - * Reading stepKey from file. - * - * @param string $stepLine - * @return string|null - */ - private function retrieveStepKey($stepLine) - { - $stepKey = null; - list($filePath, $stepLine) = explode(":", $stepLine); - $stepLine = $stepLine - 1; - - 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']; - } - - 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 $name => $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 881b677dc..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; } /** @@ -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/MftfApplicationConfig.php b/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php index 3aad73a06..7685ac919 100644 --- a/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php +++ b/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php @@ -135,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, @@ -159,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(); } 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 edf490669..a9dc6785c 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php +++ b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php @@ -124,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); @@ -159,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) { @@ -228,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 = []; 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 2d00e0abd..0c209d8f2 100644 --- a/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php @@ -364,4 +364,29 @@ protected function applyAllFailed() } 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 afeacaad3..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.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); @@ -112,6 +113,8 @@ function ($type, $buffer) use ($output) { $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 index 3992aa677..1a469b179 100644 --- a/src/Magento/FunctionalTestingFramework/Console/Codecept/CodeceptCommandUtil.php +++ b/src/Magento/FunctionalTestingFramework/Console/Codecept/CodeceptCommandUtil.php @@ -23,6 +23,7 @@ class CodeceptCommandUtil */ private $cwd = null; + // @codingStandardsIgnoreStart /** * Setup Codeception * @@ -39,6 +40,7 @@ public function setup(InputInterface $input) return $input->setTokens($tokens); }, null, ArgvInput::class); } + // @codingStandardsIgnoreEnd /** * Save Codeception working directory diff --git a/src/Magento/FunctionalTestingFramework/Console/CodeceptRunCommand.php b/src/Magento/FunctionalTestingFramework/Console/CodeceptRunCommand.php index de16bad84..35e5ddc36 100644 --- a/src/Magento/FunctionalTestingFramework/Console/CodeceptRunCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/CodeceptRunCommand.php @@ -20,7 +20,7 @@ class CodeceptRunCommand extends Run * * @return void */ - protected function configure() + protected function configure():void { $this->setName('codecept:run') ->setDescription( diff --git a/src/Magento/FunctionalTestingFramework/Console/CommandList.php b/src/Magento/FunctionalTestingFramework/Console/CommandList.php index 840e7dd86..a31ebe175 100644 --- a/src/Magento/FunctionalTestingFramework/Console/CommandList.php +++ b/src/Magento/FunctionalTestingFramework/Console/CommandList.php @@ -36,6 +36,7 @@ public function __construct(array $commands = []) '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 aa0c81d57..d55aedd25 100644 --- a/src/Magento/FunctionalTestingFramework/Console/DoctorCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/DoctorCommand.php @@ -190,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; } @@ -198,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/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 e783415f3..c1ac3aa6e 100644 --- a/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php @@ -11,18 +11,23 @@ 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\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) @@ -30,6 +35,35 @@ 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. @@ -69,7 +103,7 @@ protected function configure() '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, @@ -78,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(); @@ -92,6 +137,7 @@ protected function configure() * @return void|integer * @throws TestFrameworkException * @throws FastFailException + * @throws XmlException */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -107,11 +153,19 @@ protected function execute(InputInterface $input, OutputInterface $output) $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( @@ -127,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); } @@ -176,6 +234,8 @@ protected function execute(InputInterface $input, OutputInterface $output) SuiteGenerator::getInstance()->generateAllSuites($testManifest); $testManifest->generate(); + + SuiteGenerator::getInstance()->generateTestgroupmembership($testManifest); } catch (\Exception $e) { if (!empty(GenerationErrorHandler::getInstance()->getAllErrors())) { GenerationErrorHandler::getInstance()->printErrorSummary(); @@ -189,6 +249,24 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } + // 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; @@ -231,7 +309,8 @@ private function createTestConfiguration( $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) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE + ) { print($message); } GenerationErrorHandler::getInstance()->addError('test', $test, $message); @@ -309,4 +388,234 @@ private function parseConfigParallelOptions($time, $groups) 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 89b184676..3b75cbb87 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunManifestCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunManifestCommand.php @@ -86,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - if ($line == count($manifestFile) - 1) { + if ($line === count($manifestFile) - 1) { $this->runManifestLine($manifestFile[$line], $output, true); } else { $this->runManifestLine($manifestFile[$line], $output); @@ -131,7 +131,7 @@ private function runManifestLine($manifestLine, $output, $exit = false) . " run functional --verbose --steps " . $manifestLine; // run the codecept command in a sub process - $process = new Process($codeceptionCommand); + $process = Process::fromShellCommandline($codeceptionCommand); $process->setWorkingDirectory(TESTS_BP); $process->setIdleTimeout(600); $process->setTimeout(0); diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php index fcf04cb3d..31ca307d5 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php @@ -20,6 +20,9 @@ use Symfony\Component\Process\Process; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +/** + * @SuppressWarnings(PHPMD) + */ class RunTestCommand extends BaseGenerateCommand { /** @@ -38,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(); } @@ -63,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'); @@ -86,7 +99,22 @@ 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; @@ -98,7 +126,8 @@ 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); @@ -110,11 +139,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $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); } // Add all failed tests in 'failed' file @@ -128,12 +157,15 @@ 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) { + $xml = ($input->getOption('xml')) ? '--xml' : ""; + $noAnsi = ($input->getOption('no-ansi')) ? '--no-ansi' : ""; if ($this->pauseEnabled()) { $codeceptionCommand = self::CODECEPT_RUN_FUNCTIONAL; } else { @@ -155,16 +187,18 @@ private function runTests(array $tests, OutputInterface $output) } if ($this->pauseEnabled()) { - $fullCommand = $codeceptionCommand . $testsDirectory . $testName . ' --verbose --steps --debug'; - if ($i != count($tests) - 1) { + $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'; - $this->returnCode = max($this->returnCode, $this->executeTestCommand($fullCommand, $output)); + $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(); } @@ -175,16 +209,19 @@ 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) { + $xml = ($input->getOption('xml')) ? '--xml' : ""; + $noAnsi = ($input->getOption('no-ansi')) ? '--no-ansi' : ""; if ($this->pauseEnabled()) { - $codeceptionCommand = self::CODECEPT_RUN_FUNCTIONAL . '--verbose --steps --debug'; + $codeceptionCommand = self::CODECEPT_RUN_FUNCTIONAL . '--verbose --steps --debug '.$xml; } else { $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') - . ' run functional --verbose --steps '; + . ' run functional --verbose --steps '.$xml; } $count = count($suitesConfig); @@ -195,14 +232,16 @@ private function runTestsInSuite(array $suitesConfig, OutputInterface $output) $index += 1; if ($this->pauseEnabled()) { - if ($index != $count) { + 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)); + $this->returnCode = max($this->returnCode, $this->executeTestCommand($fullCommand, $output, $noAnsi)); + } + if (!empty($xml)) { + $this->movingXMLFileFromSourceToDestination($xml, $suite, $output); } - // Save failed tests $this->appendRunFailed(); } @@ -217,15 +256,31 @@ private function runTestsInSuite(array $suitesConfig, OutputInterface $output) * * @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 1652a896d..55834ad9b 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php @@ -7,31 +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 $testsReRunFile; - - /** - * @var string - */ - private $testsManifestFile; + private $testsReRunFile = "rerun_tests"; /** * @var array @@ -66,49 +53,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $this->testsFailedFile = $this->getTestsOutputDir() . self::FAILED_FILE; $this->testsReRunFile = $this->getTestsOutputDir() . "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 + + $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; for ($i = 0; $i < count($testManifestList); $i++) { if ($this->pauseEnabled()) { $codeceptionCommand = self::CODECEPT_RUN_FUNCTIONAL . $testManifestList[$i] . ' --debug '; - if ($i != count($testManifestList) - 1) { + if ($i !== count($testManifestList) - 1) { $codeceptionCommand .= self::CODECEPT_RUN_OPTION_NO_EXIT; } $returnCode = $this->codeceptRunTest($codeceptionCommand, $output); @@ -116,7 +73,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional '; $codeceptionCommand .= $testManifestList[$i]; - $process = new Process($codeceptionCommand); + $process = Process::fromShellCommandline($codeceptionCommand); $process->setWorkingDirectory(TESTS_BP); $process->setIdleTimeout(600); $process->setTimeout(0); @@ -125,6 +82,8 @@ function ($type, $buffer) use ($output) { $output->write($buffer); } )); + $process->__destruct(); + unset($process); } if (file_exists($this->testsFailedFile)) { @@ -143,85 +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 { - $suiteName = $this->sanitizeSuiteName($suiteName); - $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; - } - /** - * 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 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 a6302dacd..fefbe08f8 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php @@ -7,9 +7,8 @@ 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; @@ -17,7 +16,6 @@ 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 { @@ -29,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', @@ -58,6 +64,9 @@ protected function configure() */ 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'); @@ -104,9 +113,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($this->pauseEnabled()) { - $commandString = self::CODECEPT_RUN_FUNCTIONAL . '--verbose --steps --debug'; + $commandString = self::CODECEPT_RUN_FUNCTIONAL . '--verbose --steps --debug '.$xml; } else { - $commandString = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional --verbose --steps'; + $commandString = realpath( + PROJECT_ROOT . '/vendor/bin/codecept' + ) . ' run functional --verbose --steps '.$xml; } $exitCode = -1; @@ -115,12 +126,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $codeceptionCommandString = $commandString . ' -g ' . $groups[$i]; if ($this->pauseEnabled()) { - if ($i != count($groups) - 1) { + if ($i !== count($groups) - 1) { $codeceptionCommandString .= self::CODECEPT_RUN_OPTION_NO_EXIT; } $returnCodes[] = $this->codeceptRunTest($codeceptionCommandString, $output); } else { - $process = new Process($codeceptionCommandString); + $process = Process::fromShellCommandline($codeceptionCommandString); $process->setWorkingDirectory(TESTS_BP); $process->setIdleTimeout(600); $process->setTimeout(0); @@ -130,7 +141,9 @@ function ($type, $buffer) use ($output) { } ); } - + if (!empty($xml)) { + $this->movingXMLFileFromSourceToDestination($xml, $groups[$i].'_'.'group', $output); + } // Save failed tests $this->appendRunFailed(); } @@ -139,7 +152,7 @@ function ($type, $buffer) use ($output) { $this->applyAllFailed(); foreach ($returnCodes as $returnCode) { - if ($returnCode != 0) { + if ($returnCode !== 0) { return $returnCode; } $exitCode = 0; 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 806508ef8..e8d67e04b 100644 --- a/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php @@ -117,7 +117,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $staticOutput = $staticCheck->getOutput(); LoggingUtil::getInstance()->getLogger(get_class($staticCheck))->info($staticOutput); - $this->ioStyle->text($staticOutput); + $this->ioStyle->text($staticOutput??""); $this->ioStyle->text('Total execution time is ' . (string)($end - $start) . ' seconds.' . PHP_EOL); } 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 bc19c35aa..830abe6c9 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php @@ -84,7 +84,7 @@ public function initDom($xml, $filename = null) $itemNodes = $dom->getElementsByTagName('item'); /** @var \DOMElement $itemNode */ foreach ($itemNodes as $itemKey => $itemNode) { - if ($itemNode->hasAttribute("name") == false) { + if ($itemNode->hasAttribute("name") === false) { $itemNode->setAttribute("name", (string)$itemKey); } } 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 87dc6a13d..da7888388 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php @@ -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/PersistedObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php index c1f5d8d85..e9f905a6b 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php @@ -106,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; @@ -170,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; @@ -214,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; } 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 ef1a7faa5..ab5879301 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php @@ -167,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; @@ -351,7 +351,7 @@ public function addQueryParams() */ public function logDeprecated() { - if ($this->deprecated != null) { + if ($this->deprecated !== null) { LoggingUtil::getInstance()->getLogger(self::class)->deprecation( $message = "The operation {$this->name} is deprecated.", ["operationType" => $this->operation, "deprecatedMessage" => $this->deprecated], 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 e4dc5f267..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,7 +244,7 @@ private function getDependentEntitiesOfType($type) */ private function resolveOperationObjectAndEntityData($entityObject, $operationElementValue) { - if ($operationElementValue != $entityObject->getType()) { + 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); @@ -275,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, @@ -403,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') { @@ -449,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(), @@ -476,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 986f087e3..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() . diff --git a/src/Magento/FunctionalTestingFramework/DataTransport/AdminFormExecutor.php b/src/Magento/FunctionalTestingFramework/DataTransport/AdminFormExecutor.php index 5639ee8d7..105cce7e7 100644 --- a/src/Magento/FunctionalTestingFramework/DataTransport/AdminFormExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/AdminFormExecutor.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\DataTransport; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use Magento\FunctionalTestingFramework\Util\MftfGlobals; use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlTransport; @@ -73,9 +74,11 @@ private function authorize() // Authenticate admin user $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); @@ -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/Clock.php b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/Clock.php new file mode 100644 index 000000000..2961d1641 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/Clock.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\DataTransport\Auth\Tfa; + +use DateTimeImmutable; +use Psr\Clock\ClockInterface; + +class Clock implements ClockInterface +{ + /** + * Return DateTimeImmutable class object + * + * @return DateTimeImmutable + */ + public function now(): DateTimeImmutable + { + return new DateTimeImmutable(); + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/OTP.php b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/OTP.php index 32c1f6b81..a92ba65fa 100644 --- a/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/OTP.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/OTP.php @@ -31,7 +31,7 @@ class OTP * @return string * @throws TestFrameworkException */ - public static function getOTP($path = null) + public static function getOTP(?string $path = null) { if ($path === null) { $path = self::OTP_SHARED_SECRET_PATH; @@ -57,7 +57,14 @@ private static function create($path) throw new TestFrameworkException('Unable to get OTP' . PHP_EOL . $e->getMessage()); } - self::$totps[$path] = TOTP::create($secret); + self::$totps[$path] = TOTP::create( + $secret, + TOTP::DEFAULT_PERIOD, + TOTP::DEFAULT_DIGEST, + TOTP::DEFAULT_DIGITS, + TOTP::DEFAULT_EPOCH, + new Clock() + ); self::$totps[$path]->setIssuer('MFTF'); self::$totps[$path]->setLabel('MFTF Testing'); } diff --git a/src/Magento/FunctionalTestingFramework/DataTransport/Auth/WebApiAuth.php b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/WebApiAuth.php index be29a7e19..ac2d3ae99 100644 --- a/src/Magento/FunctionalTestingFramework/DataTransport/Auth/WebApiAuth.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/WebApiAuth.php @@ -6,6 +6,7 @@ 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; @@ -53,18 +54,26 @@ class WebApiAuth * @throws FastFailException * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ - public static function getAdminToken($username = null, $password = null) + public static function getAdminToken(?string $username = null, ?string $password = null) { $login = $username ?? getenv('MAGENTO_ADMIN_USERNAME'); - $password = $password ?? getenv('MAGENTO_ADMIN_PASSWORD'); + 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' => getenv('MAGENTO_ADMIN_PASSWORD'), + 'MAGENTO_ADMIN_PASSWORD' => $secret, ]; throw new FastFailException($message, $context); } @@ -113,14 +122,12 @@ public static function getAdminToken($username = null, $password = null) $errMessage = $e->getMessage(); } - $message = 'Cannot retrieve API token with credentials. Please check the following configurations'; + $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 .= "username: {$login}" . PHP_EOL; - $message .= "password: {$password}" . PHP_EOL; $message .= $errMessage; $context = ['url' => $authUrl]; throw new FastFailException($message, $context); diff --git a/src/Magento/FunctionalTestingFramework/DataTransport/FrontendFormExecutor.php b/src/Magento/FunctionalTestingFramework/DataTransport/FrontendFormExecutor.php index 0cf69ec94..c167c79b6 100644 --- a/src/Magento/FunctionalTestingFramework/DataTransport/FrontendFormExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/FrontendFormExecutor.php @@ -165,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/DataTransport/Protocol/CurlInterface.php b/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlInterface.php index d15dbb18b..31d931819 100644 --- a/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlInterface.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlInterface.php @@ -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/DataTransport/Protocol/CurlTransport.php b/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlTransport.php index a34bad6ce..fc35da08e 100644 --- a/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlTransport.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlTransport.php @@ -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 index 1bf792003..52b7cdb65 100644 --- a/src/Magento/FunctionalTestingFramework/DataTransport/WebApiExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/WebApiExecutor.php @@ -48,7 +48,7 @@ class WebApiExecutor implements CurlInterface * @param string $storeCode * @throws FastFailException */ - public function __construct($storeCode = null) + public function __construct(?string $storeCode = null) { $this->storeCode = $storeCode; $this->transport = new CurlTransport(); @@ -98,7 +98,7 @@ public function write($url, $data = [], $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) { return $this->transport->read(); } @@ -135,7 +135,7 @@ public function close() protected function getFormattedUrl($resource) { $urlResult = MftfGlobals::getWebApiBaseUrl(); - if ($this->storeCode != null) { + if ($this->storeCode !== null) { $urlResult .= $this->storeCode . '/'; } $urlResult .= trim($resource, '/'); diff --git a/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php index e4f433a2a..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"; @@ -44,7 +79,7 @@ class TestContextExtension extends BaseExtension * @return void * @throws \Exception */ - public function _initialize() + public function _initialize(): void { $events = [ Events::TEST_START => 'testStart', @@ -72,7 +107,10 @@ public function testStart(\Codeception\Event\TestEvent $e) CURLOPT_URL => getenv('MAGENTO_BASE_URL') . "/test.php?test=" . $this->currentTest, ]); curl_exec($cURLConnection); - curl_close($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(); @@ -92,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 )); @@ -100,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()); } } } @@ -109,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 + )); } /** @@ -128,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"]; } } @@ -159,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); @@ -185,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. @@ -199,6 +444,13 @@ public function beforeStep(\Codeception\Event\StepEvent $e) */ public function afterStep(\Codeception\Event\StepEvent $e) { + $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"); @@ -233,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)) { @@ -257,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/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 f2f818bd4..000000000 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php +++ /dev/null @@ -1,100 +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 -{ - /** - * List of known PWA loading masks by selector - * - * Overriding the MagentoWebDriver array to contain applicable PWA locators. - * - * @var array - */ - protected $loadingMasksLocators = [ - '//div[contains(@class, "indicator-global-")]', - '//div[contains(@class, "indicator-root-")]', - '//img[contains(@class, "indicator-indicator-")]', - '//span[contains(@class, "indicator-message-")]' - ]; - - /** - * 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 - * @param integer $timeout - * @throws \Exception - * @return void - */ - public function amOnPage($page, $timeout = null) - { - WebDriver::amOnPage($page); - $this->waitForLoadingMaskToDisappear($timeout); - } - - /** - * Wait for a PWA Element to NOT be visible using JavaScript. - * Add the WAIT_TIMEOUT variable to your .env file for this action. - * - * @param string $selector - * @param integer $timeout - * @throws \Exception - * @return void - */ - public function waitForPwaElementNotVisible($selector, $timeout = null) - { - $timeout = $timeout ?? $this->_getConfig()['pageload_timeout']; - - // Determine what type of Selector is used. - // Then use the correct JavaScript to locate the Element. - if (\Codeception\Util\Locator::isCss($selector)) { - $this->waitForLoadingMaskToDisappear($timeout); - $this->waitForJS("return !document.querySelector(`$selector`);", $timeout); - } else { - $this->waitForLoadingMaskToDisappear($timeout); - $this->waitForJS("return !document.evaluate(`$selector`, document, null, - XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;", $timeout); - } - } - - /** - * Wait for a PWA Element to be visible using JavaScript. - * Add the WAIT_TIMEOUT variable to your .env file for this action. - * - * @param string $selector - * @param integer $timeout - * @throws \Exception - * @return void - */ - public function waitForPwaElementVisible($selector, $timeout = null) - { - $timeout = $timeout ?? $this->_getConfig()['pageload_timeout']; - - // Determine what type of Selector is used. - // Then use the correct JavaScript to locate the Element. - if (\Codeception\Util\Locator::isCss($selector)) { - $this->waitForLoadingMaskToDisappear($timeout); - $this->waitForJS("return !!document && !!document.querySelector(`$selector`);", $timeout); - } else { - $this->waitForLoadingMaskToDisappear($timeout); - $this->waitForJS("return !!document && !!document.evaluate(`$selector`, document, null, - XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;", $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 7a3220921..e7350f5b9 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php @@ -21,13 +21,13 @@ use Magento\FunctionalTestingFramework\DataTransport\Auth\Tfa\OTP; use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; +use Magento\FunctionalTestingFramework\Module\Util\ModuleUtils; use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; use Magento\FunctionalTestingFramework\Util\ConfigSanitizerUtil; -use Yandex\Allure\Adapter\AllureException; +use Qameta\Allure\Allure; use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlTransport; -use Yandex\Allure\Adapter\Support\AttachmentSupport; +use Qameta\Allure\Io\DataSourceFactory; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; /** * MagentoWebDriver module provides common Magento web actions through Selenium WebDriver. @@ -53,7 +53,6 @@ */ class MagentoWebDriver extends WebDriver { - use AttachmentSupport; use Pause { pause as codeceptPause; } @@ -72,19 +71,9 @@ class MagentoWebDriver extends WebDriver '//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"]', ]; /** @@ -144,6 +133,7 @@ class MagentoWebDriver extends WebDriver public function _initialize() { $this->config = ConfigSanitizerUtil::sanitizeWebDriverConfig($this->config); + parent::_initialize(); $this->cleanJsError(); } @@ -153,7 +143,7 @@ public function _initialize() * * @return void */ - public function _resetConfig() + public function _resetConfig():void { parent::_resetConfig(); $this->config = ConfigSanitizerUtil::sanitizeWebDriverConfig($this->config); @@ -219,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); } /** @@ -234,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"; @@ -249,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"; @@ -264,9 +252,8 @@ 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"; @@ -280,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) { @@ -303,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"; @@ -318,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"; @@ -333,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->assertStringContainsString($needle, $actualUrl); + $this->assertStringContainsString(urldecode($needle), urldecode($actualUrl)); } /** @@ -453,7 +437,7 @@ public function waitForPageLoad($timeout = null) public function waitForLoadingMaskToDisappear($timeout = null) { $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. @@ -509,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) { @@ -546,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 @@ -571,7 +555,7 @@ public function magentoCLI($command, $timeout = null, $arguments = null) $apiURL, [ 'token' => WebApiAuth::getAdminToken(), - getenv('MAGENTO_CLI_COMMAND_PARAMETER') => $command, + getenv('MAGENTO_CLI_COMMAND_PARAMETER') => urlencode($command), 'arguments' => $arguments, 'timeout' => $timeout, ], @@ -581,7 +565,9 @@ public function magentoCLI($command, $timeout = null, $arguments = null) $response = $executor->read(); $executor->close(); - return $response; + $util = new ModuleUtils(); + $response = trim($util->utf8SafeControlCharacterTrim($response)); + return $response != "" ? $response : "CLI did not return output."; } /** @@ -708,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, ""); } @@ -758,7 +744,7 @@ 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); @@ -787,6 +773,54 @@ public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null) } } + /** + * 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. @@ -812,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 */ @@ -830,6 +864,27 @@ public function magentoCLISecret($command, $timeout = null, $arguments = null) return $this->magentoCLI($decryptedCommand, $timeout, $arguments); } + /** + * Function used to verify sensitive credentials in the data, data is decrypted immediately prior to see to avoid + * exposure in console or log. + * + * @param string $field + * @param string $value + * @return void + * @throws TestFrameworkException + */ + public function seeInSecretField(string $field, string $value):void + { + // to protect any secrets from being printed to console the values are executed only at the webdriver level as a + // decrypted value + + $decryptedValue = CredentialStore::getInstance()->decryptSecretValue($value); + if ($decryptedValue === false) { + throw new TestFrameworkException("\nFailed to decrypt value {$value} for field {$field}\n"); + } + $this->seeInField($field, $decryptedValue); + } + /** * Override for _failed method in Codeception method. Adds png and html attachments to allure report * following parent execution of test failure processing. @@ -849,13 +904,21 @@ public function _failed(TestInterface $test, $fail) } } - 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}"); @@ -869,7 +932,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); } @@ -886,7 +949,7 @@ public function saveScreenshot() * @return void * @throws \Exception */ - public function amOnPage($page) + public function amOnPage($page):void { (0 === strpos($page, 'http')) ? parent::amOnUrl($page) : parent::amOnPage($page); $this->waitForPageLoad(); @@ -950,9 +1013,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_")); @@ -992,6 +1054,10 @@ private function executeCronjobs($cronGroups, $timeout, $arguments): string { $cronGroups = array_filter($cronGroups); + if (isset($cronGroups[0]) && !isset($cronGroups[1])) { + $arguments .= ' --bootstrap=standaloneProcessStarted=1'; + } + $waitFor = $this->getCronWait($cronGroups); if ($waitFor) { @@ -1018,7 +1084,7 @@ private function executeCronjobs($cronGroups, $timeout, $arguments): string * @return void * @throws \Exception */ - public function switchToIFrame($locator = null) + public function switchToIFrame($locator = null):void { try { parent::switchToIFrame($locator); @@ -1038,8 +1104,14 @@ public function switchToIFrame($locator = null) * @param boolean $pauseOnFail * @return void */ - public function pause($pauseOnFail = false) + public function pause($pauseOnFail = false):void { + 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; } @@ -1050,4 +1122,66 @@ public function pause($pauseOnFail = false) $this->codeceptPause(); } + + /** + * @param string $selector + * @param string $expected + * @return void + */ + public function seeNumberOfElements($selector, $expected): void + { + $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' + ); + } + } + + /** + * @param string $text + * @param string $selector + * @return void + */ + public function see($text, $selector = null): void + { + $text = (isset($text)) + ? (string)$text + : ""; + if (!$selector) { + $this->assertPageContains($text); + return; + } + + $this->enableImplicitWait(); + $nodes = $this->matchVisible($selector); + $this->disableImplicitWait(); + $this->assertNodesContain($text, $nodes, $selector); + } + + /** + * @param string $text + * @param string $selector + * @return void + */ + public function dontSee($text, $selector = null): void + { + $text = (isset($text)) + ? (string)$text + : ""; + if (!$selector) { + $this->assertPageNotContains($text); + } else { + $nodes = $this->matchVisible($selector); + $this->assertNodesNotContain($text, $nodes, $selector); + } + } } 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/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 8f4302077..dd723fa1b 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php @@ -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, "/"); } 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 84% rename from src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupArgumentsCheck.php rename to src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupStandardsCheck.php index 8b95fb9d5..fdc90b6d9 100644 --- a/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupArgumentsCheck.php +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupStandardsCheck.php @@ -14,16 +14,18 @@ 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_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. @@ -98,8 +100,32 @@ private function findErrorsInFileSet($files) $actionGroupErrors = []; /** @var SplFileInfo $filePath */ foreach ($files as $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); diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/AnnotationsCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/AnnotationsCheck.php index e7a0b7e54..0b3e6c40d 100644 --- a/src/Magento/FunctionalTestingFramework/StaticCheck/AnnotationsCheck.php +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/AnnotationsCheck.php @@ -173,8 +173,8 @@ private function validateSkipIssueId($test) $skip = $annotations['skip'] ?? null; if ($skip !== null) { $validateSkipped = true; - if ((!isset($skip[0]) || strlen($skip[0]) == 0) - && (!isset($skip['issueId']) || strlen($skip['issueId']) == 0)) { + 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."; } } 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/StaticChecksList.php b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php index 07c52ce8c..0cb6b4a73 100644 --- a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php @@ -18,6 +18,11 @@ 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'; /** @@ -44,11 +49,15 @@ public function __construct(array $checks = []) { $this->checks = [ 'testDependencies' => new TestDependencyCheck(), - 'actionGroupArguments' => new ActionGroupArgumentsCheck(), + 'actionGroupArguments' => new ActionGroupStandardsCheck(), self::DEPRECATED_ENTITY_USAGE_CHECK_NAME => new DeprecatedEntityUsageCheck(), 'annotations' => new AnnotationsCheck(), - self::PAUSE_ACTION_USAGE_CHECK_NAME => new PauseActionUsageCheck() - ] + $checks; + 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) { diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php index 6bfe17219..01660c427 100644 --- a/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php @@ -13,6 +13,7 @@ use Symfony\Component\Finder\Finder; use Exception; use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Magento\FunctionalTestingFramework\Util\Script\TestDependencyUtil; /** * Class TestDependencyCheck @@ -23,14 +24,11 @@ class TestDependencyCheck implements StaticCheckInterface const EXTENDS_REGEX_PATTERN = '/extends=["\']([^\'"]*)/'; const ACTIONGROUP_REGEX_PATTERN = '/ref=["\']([^\'"]*)/'; - 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 @@ -51,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. @@ -81,6 +84,16 @@ class TestDependencyCheck implements StaticCheckInterface */ 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 * @@ -91,6 +104,7 @@ class TestDependencyCheck implements StaticCheckInterface public function execute(InputInterface $input) { $this->scriptUtil = new ScriptUtil(); + $this->testDependencyUtil = new TestDependencyUtil(); $allModules = $this->scriptUtil->getAllModulePaths(); if (!class_exists('\Magento\Framework\Component\ComponentRegistrar')) { @@ -98,10 +112,27 @@ public function execute(InputInterface $input) "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, @@ -124,13 +155,20 @@ public function execute(InputInterface $input) 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; } @@ -139,9 +177,9 @@ 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??""; } /** @@ -150,14 +188,14 @@ public function getOutput() * @return array * @throws XmlException */ - private function findErrorsInFileSet($files) + private function findErrorsInFileSet(Finder $files): array { $testErrors = []; foreach ($files as $filePath) { $this->allEntities = []; - $moduleName = $this->getModuleName($filePath); + $moduleName = $this->testDependencyUtil->getModuleName($filePath, $this->moduleNameToPath); // Not a module, is either dev/tests/acceptance or loose folder with test materials - if ($moduleName == null) { + if ($moduleName === null) { continue; } @@ -199,6 +237,7 @@ private function findErrorsInFileSet($files) // Find violating references and set error output $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; } @@ -209,22 +248,31 @@ private function findErrorsInFileSet($files) * @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; } } @@ -235,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 = []; @@ -255,111 +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) { - $moduleName = $this->getModuleName($file); - if (isset($this->moduleNameToComposerName[$moduleName])) { - $composerModuleName = $this->moduleNameToComposerName[$moduleName]; - $filenames[$item->getName()][] = $composerModuleName; - } - } - } - return $filenames; - } - - /** - * Return module name for a file path - * - * @param string $filePath - * @return string|null - */ - private function getModuleName($filePath) - { - $moduleName = null; - foreach ($this->moduleNameToPath as $name => $path) { - if (strpos($filePath, $path) !== false) { - $moduleName = $name; - break; - } - } - return $moduleName; - } } 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/Suite/Generators/GroupClassGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php index f90a25322..e160c4875 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php @@ -152,7 +152,7 @@ private function buildHookMustacheArray($hookObj) } // 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()]; } @@ -213,7 +213,7 @@ 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 { @@ -278,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 f5d9e45dd..75e86b3a8 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php @@ -62,7 +62,7 @@ private function __clone() */ public static function getInstance(): ObjectHandlerInterface { - if (self::$instance == null) { + if (self::$instance === null) { self::$instance = new SuiteObjectHandler(); self::$instance->initSuiteData(); } @@ -80,7 +80,7 @@ public function getObject($objectName): SuiteObject { if (!array_key_exists($objectName, $this->suiteObjects)) { throw new TestReferenceException( - "Suite ${objectName} is not defined in xml or is invalid." + "Suite {$objectName} is not defined in xml or is invalid." ); } return $this->suiteObjects[$objectName]; 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 1f8fec586..4c007ed95 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php @@ -8,12 +8,12 @@ use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; use Magento\FunctionalTestingFramework\Exceptions\FastFailException; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; 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; @@ -21,7 +21,6 @@ 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; /** @@ -140,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 @@ -207,7 +308,7 @@ private function generateSuiteFromTest($suiteName, $tests = [], $originalSuiteNa $this->appendEntriesToConfig($suiteName, $fullPath, $groupNamespace); if (MftfApplicationConfig::getConfig()->verboseEnabled() - && MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::GENERATION_PHASE) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { print("suite {$suiteName} generated\n"); } LoggingUtil::getInstance()->getLogger(self::class)->info( @@ -268,7 +369,7 @@ private function generateSplitSuiteFromTest($suiteName, $suiteContent) try { $this->generateSuiteFromTest($suiteSplitName, $tests, $suiteName); } catch (FastFailException $e) { - throw $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. @@ -305,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); } } @@ -331,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); } /** @@ -356,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); } /** @@ -406,37 +473,6 @@ private static function clearPreviousGroupPreconditions() array_map('unlink', glob("$groupFilePath*.php")); } - /** - * Function to return contents of codeception.yml file for config changes. - * - * @return array - */ - private static function getYamlFileContents() - { - $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); - } - - 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); - } - /** * Log error and throw collected exceptions * diff --git a/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php index 247286f6d..6af9070ba 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php @@ -92,7 +92,7 @@ public function parseSuiteDataIntoObjects($parsedSuiteData) $includeTests = $include['objects'] ?? []; $stepError = $include['status'] ?? 0; $includeMessage = ''; - if ($stepError != 0) { + if ($stepError !== 0) { $includeMessage = "ERROR: " . strval($stepError) . " test(s) not included for suite " . $parsedSuite[self::NAME]; } @@ -127,7 +127,8 @@ public function parseSuiteDataIntoObjects($parsedSuiteData) if (!empty($includeMessage)) { LoggingUtil::getInstance()->getLogger(self::class)->error($includeMessage); if (MftfApplicationConfig::getConfig()->verboseEnabled() - && MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::GENERATION_PHASE) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE + ) { print($includeMessage); } @@ -145,7 +146,7 @@ public function parseSuiteDataIntoObjects($parsedSuiteData) "Unable to parse suite " . $parsedSuite[self::NAME] . "\n" . $e->getMessage() ); if (MftfApplicationConfig::getConfig()->verboseEnabled() - && MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::GENERATION_PHASE) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { print("ERROR: Unable to parse suite " . $parsedSuite[self::NAME] . "\n"); } @@ -183,7 +184,7 @@ 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') { + if ($parsedSuite[self::NAME] === 'default') { throw new FastFailException("A Suite can not have the name \"default\""); } @@ -235,7 +236,7 @@ private function parseObjectHooks($parsedSuite) $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] @@ -254,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()) diff --git a/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache b/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache index dc2fde915..599b5791e 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache +++ b/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache @@ -3,6 +3,7 @@ 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; @@ -11,6 +12,7 @@ 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. @@ -65,6 +67,8 @@ class {{suiteName}} extends \Codeception\GroupObject private function executePreConditions() { if ($this->currentTestRun == 1) { + $this->testCount = $this->getTestCount(); + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); try { @@ -101,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 )); @@ -109,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"); @@ -150,8 +154,16 @@ class {{suiteName}} extends \Codeception\GroupObject ); $availableSessions = RemoteWebDriver::getAllSessions($wdHost); foreach ($availableSessions as $session) { - $remoteWebDriver = RemoteWebDriver::createBySessionID($session['id'], $wdHost); - $remoteWebDriver->quit(); + 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 + } } } } @@ -171,4 +183,28 @@ class {{suiteName}} extends \Codeception\GroupObject } 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/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 5a87a32f6..fb4dac688 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php @@ -151,9 +151,9 @@ private function initActionGroups() 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 4abc26c36..2361c5938 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php @@ -86,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]; @@ -124,7 +124,7 @@ public function getAllObjects() if ($errCount > 0 && MftfApplicationConfig::getConfig()->verboseEnabled() - && MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::GENERATION_PHASE) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { print( "ERROR: " . strval($errCount) @@ -173,7 +173,7 @@ public function getTestsByGroup($groupName) if ($errCount > 0 && MftfApplicationConfig::getConfig()->verboseEnabled() - && MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::GENERATION_PHASE) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { print( "ERROR: " . strval($errCount) @@ -248,7 +248,7 @@ private function initTestData($validateAnnotations = true) "Unable to parse test " . $testName . "\n" . $exception->getMessage() ); if (MftfApplicationConfig::getConfig()->verboseEnabled() - && MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::GENERATION_PHASE) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { print("ERROR: Unable to parse test " . $testName . "\n"); } GenerationErrorHandler::getInstance()->addError( @@ -276,7 +276,7 @@ private function initTestData($validateAnnotations = true) private function extendTest($testObject) { if ($testObject->getParentName() !== null) { - if ($testObject->getParentName() == $testObject->getName()) { + if ($testObject->getParentName() === $testObject->getName()) { throw new TestFrameworkException( "Mftf Test can not extend from itself: " . $testObject->getName() ); diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php index 74aa414a8..593320d44 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php @@ -31,6 +31,7 @@ class ActionGroupObject "createData", "grabAttributeFrom", "grabCookie", + "grabCookieAttributes", "grabFromCurrentUrl", "grabMultiple", "grabPageSource", @@ -239,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], @@ -531,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 8e4aa973c..d8ed23098 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -184,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. * @@ -289,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); } } @@ -417,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 @@ -561,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()}\"", @@ -592,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(); @@ -638,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) . "'", @@ -656,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()}'", @@ -697,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]; @@ -726,7 +754,7 @@ private function resolveParameterization($isParameterized, $replacement, $match, } else { $resolvedReplacement = $replacement; } - if (get_class($object) == PageObject::class && $object->getArea() == PageObject::ADMIN_AREA) { + if (get_class($object) === PageObject::class && $object->getArea() === PageObject::ADMIN_AREA) { $urlSegments = [ '{{_ENV.MAGENTO_BACKEND_BASE_URL}}', '{{_ENV.MAGENTO_BACKEND_NAME}}', @@ -794,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; @@ -810,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 82bb04906..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 * @@ -85,6 +107,13 @@ class TestObject */ private $deprecated; + /** + * Indicates if a test contains an action that requires Web API authentication. + * + * @var boolean + */ + private $hasWebApiAuthAction; + /** * TestObject constructor. * @@ -112,6 +141,7 @@ public function __construct( $this->filename = $filename; $this->parentTest = $parentTest; $this->deprecated = $deprecated; + $this->hasWebApiAuthAction = false; } /** @@ -174,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; } @@ -222,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. * @@ -237,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) { @@ -275,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/ActionMergeUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php index 28a7d405d..acc1cce16 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php @@ -26,8 +26,9 @@ class ActionMergeUtil const DEFAULT_SKIP_ON_ORDER = 'before'; const DEFAULT_SKIP_OFF_ORDER = 'after'; const DEFAULT_WAIT_ORDER = 'after'; - const APPROVED_ACTIONS = ['fillField', 'magentoCLI', 'field']; - const SECRET_MAPPING = ['fillField' => 'fillSecretField', 'magentoCLI' => 'magentoCLISecret']; + const APPROVED_ACTIONS = ['fillField', 'magentoCLI', 'field', 'seeInField']; + const SECRET_MAPPING = ['fillField' => 'fillSecretField', 'magentoCLI' => 'magentoCLISecret', + 'seeInField' => 'seeInSecretField']; const CREDS_REGEX = "/{{_CREDS\.([\w|\/]+)}}/"; /** @@ -110,7 +111,7 @@ private function resolveSecretFieldAccess($resolvedActions) if ($actionHasSecretRef && !(in_array($actionType, self::APPROVED_ACTIONS))) { throw new TestReferenceException("You cannot reference secret data outside " . - "of the fillField, magentoCLI and createData actions"); + "of the fillField, magentoCLI, seeInField and createData actions"); } // Do NOT remap actions that don't need it. @@ -170,10 +171,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 8fdcf82f6..a9fb0817e 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php @@ -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; } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php index 4afddc769..8710fbb22 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php @@ -77,14 +77,14 @@ public function extractAnnotations($testAnnotations, $filename, $validateAnnotat } $annotationValues = []; // Only transform severity annotation - if ($annotationKey == "severity") { + if ($annotationKey === "severity") { $annotationObjects[$annotationKey] = $this->transformAllureSeverityToMagento( trim($annotationData[0][self::ANNOTATION_VALUE]) ); continue; } - if ($annotationKey == "skip") { + if ($annotationKey === "skip") { $annotationData = $annotationData['issueId']; if ($validateAnnotations) { $this->validateSkippedIssues($annotationData, $filename); @@ -182,7 +182,7 @@ private function validateMissingAnnotations($annotationObjects, $filename) */ public function validateStoryTitleUniqueness() { - if (MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::EXECUTION_PHASE) { + if (MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::EXECUTION_PHASE) { return; } @@ -197,7 +197,7 @@ public function validateStoryTitleUniqueness() $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) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { print('ERROR: ' . $message); } $testArray = explode(',', $tests); @@ -220,7 +220,7 @@ public function validateStoryTitleUniqueness() */ public function validateTestCaseIdTitleUniqueness() { - if (MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::EXECUTION_PHASE) { + if (MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::EXECUTION_PHASE) { return; } @@ -235,7 +235,7 @@ public function validateTestCaseIdTitleUniqueness() $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) { + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { print('ERROR: ' . $message); } $testArray = explode(',', $tests); diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php index 1be3c895f..7f6d050fb 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php @@ -98,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() . @@ -200,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; } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php index daee2ada2..510898c6c 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php @@ -99,7 +99,7 @@ public function extractTestData($testData, $validateAnnotations = true) $testAnnotations = []; $testHooks = []; $filename = $testData['filename'] ?? null; - $fileNames = explode(",", $filename); + $fileNames = explode(",", $filename ?? ''); $baseFileName = $fileNames[0]; $module = $this->modulePathExtractor->extractModuleName($baseFileName); $testReference = $testData['extends'] ?? null; 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 d68cf43db..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"/> @@ -253,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> diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/RemoveUnusedArguments.php b/src/Magento/FunctionalTestingFramework/Upgrade/RemoveUnusedArguments.php index ced1042ad..87586c181 100644 --- a/src/Magento/FunctionalTestingFramework/Upgrade/RemoveUnusedArguments.php +++ b/src/Magento/FunctionalTestingFramework/Upgrade/RemoveUnusedArguments.php @@ -6,7 +6,7 @@ namespace Magento\FunctionalTestingFramework\Upgrade; -use Magento\FunctionalTestingFramework\StaticCheck\ActionGroupArgumentsCheck; +use Magento\FunctionalTestingFramework\StaticCheck\ActionGroupStandardsCheck; use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -40,7 +40,7 @@ public function execute(InputInterface $input, OutputInterface $output) $fileSystem = new Filesystem(); foreach ($xmlFiles as $file) { $contents = $file->getContents(); - $argumentsCheck = new ActionGroupArgumentsCheck(); + $argumentsCheck = new ActionGroupStandardsCheck(); /** @var DOMElement $actionGroup */ $actionGroup = $argumentsCheck->getActionGroupDomElement($contents); $allArguments = $argumentsCheck->extractActionGroupArguments($actionGroup); diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/UpdateAssertionSchema.php b/src/Magento/FunctionalTestingFramework/Upgrade/UpdateAssertionSchema.php index 2628be851..3c6825092 100644 --- a/src/Magento/FunctionalTestingFramework/Upgrade/UpdateAssertionSchema.php +++ b/src/Magento/FunctionalTestingFramework/Upgrade/UpdateAssertionSchema.php @@ -104,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 = ""; } @@ -119,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/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/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 268bc5193..51d707305 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php @@ -36,7 +36,6 @@ public static function getInstance(): LoggingUtil if (self::$instance === null) { self::$instance = new LoggingUtil(); } - return self::$instance; } @@ -66,7 +65,7 @@ private function __clone() */ public function getLogger($className): MftfLogger { - if ($className == null) { + if ($className === null) { throw new TestFrameworkException("You must pass a class name to receive a logger"); } diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseParallelTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseParallelTestManifest.php index da0f9d353..1e9e1d109 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseParallelTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseParallelTestManifest.php @@ -41,6 +41,12 @@ abstract class BaseParallelTestManifest extends BaseTestManifest */ protected $dirPath; + /** + * An array of test name count in a single group + * @var array + */ + protected $testCountsToGroup = []; + /** * BaseParallelTestManifest constructor. * @@ -87,6 +93,8 @@ public function generate() foreach ($this->testGroups as $groupNumber => $groupContents) { $this->generateGroupFile($groupContents, $groupNumber, $suites); } + + $this->generateGroupSummaryFile($this->testCountsToGroup); } /** @@ -114,17 +122,35 @@ 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. 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 b9a74de84..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 Magento\FunctionalTestingFramework\DataTransport\Auth\WebApiAuth; use \Magento\FunctionalTestingFramework\Util\ModuleResolver\AlphabeticSequenceSorter; use \Magento\FunctionalTestingFramework\Util\ModuleResolver\SequenceSorterInterface; @@ -136,27 +138,6 @@ class ModuleResolver '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 = WebApiAuth::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(); @@ -303,100 +289,6 @@ protected function getModuleAllowlist() return array_map('trim', explode(',', $moduleAllowlist)); } - /** - * 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; - } - /** * Aggregate all code paths with test module composer json files * @@ -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; @@ -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; } @@ -694,7 +544,7 @@ private function printMagentoVersionInfo() protected function applyCustomModuleMethods($modulesPath) { $modulePathsResult = $this->removeBlocklistModules($modulesPath); - $customModulePaths = $this->getCustomModulePaths(); + $customModulePaths = ModuleResolverService::getInstance()->getCustomModulePaths(); array_map(function ($key, $value) { LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( @@ -732,27 +582,6 @@ private function removeBlocklistModules($modulePaths) return $modulePathsResult; } - /** - * Returns an array of custom module paths defined by the user - * - * @return string[] - */ - private function getCustomModulePaths() - { - $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 moduleBlocklist. * @@ -763,50 +592,6 @@ private function getModuleBlocklist() return $this->moduleBlocklist; } - /** - * 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 []; - } - /** * Find vendor and module name from path * 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 c7b60b6ab..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 + * 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 11afa5cc1..9cd02a076 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Script/ScriptUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Script/ScriptUtil.php @@ -5,6 +5,7 @@ */ namespace Magento\FunctionalTestingFramework\Util\Script; +use Exception; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; @@ -40,7 +41,7 @@ class ScriptUtil * @return array * @throws TestFrameworkException */ - public function getAllModulePaths() + public function getAllModulePaths(): array { MftfApplicationConfig::create( true, @@ -60,12 +61,45 @@ public function getAllModulePaths() * @param string $message * @return string */ - public function printErrorsToFile($errors, $filePath, $message) + public function printErrorsToFile(array $errors, string $filePath, string $message): string { if (empty($errors)) { return $message . ": No errors found."; } + $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); @@ -73,15 +107,11 @@ public function printErrorsToFile($errors, $filePath, $message) $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 {$filePath}"; - - return $output; } /** @@ -91,7 +121,7 @@ public function printErrorsToFile($errors, $filePath, $message) * @param string $scope * @return Finder|array */ - public function getModuleXmlFilesByScope($modulePaths, $scope) + public function getModuleXmlFilesByScope(array $modulePaths, string $scope) { $found = false; $scopePath = DIRECTORY_SEPARATOR . ucfirst($scope) . DIRECTORY_SEPARATOR; @@ -178,7 +208,6 @@ public function resolveEntityReferences($braceReferences, $contents, $resolveSec // trim `{{data.field}}` to `field` preg_match('/.([^.]+)}}/', $reference, $elementName); /** @var ElementObject $element */ - /** @var SectionObject $entity */ $element = $entity->getElement($elementName[1]); if ($element) { $entities[$entity->getName() . '.' . $elementName[1]] = $element; @@ -200,7 +229,7 @@ public function resolveEntityReferences($braceReferences, $contents, $resolveSec * @throws XmlException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function resolveParametrizedReferences($braceReferences, $contents, $resolveSectionElement = false) + public function resolveParametrizedReferences($braceReferences, $contents, $resolveSectionElement = false): array { $entities = []; foreach ($braceReferences as $parameterizedReference) { @@ -232,7 +261,6 @@ public function resolveParametrizedReferences($braceReferences, $contents, $reso // trim `data.field` to `field` preg_match('/.([^.]+)/', $argument, $elementName); /** @var ElementObject $element */ - /** @var SectionObject $entity */ $element = $entity->getElement($elementName[1]); if ($element) { $entities[$entity->getName() . '.' . $elementName[1]] = $element; @@ -252,7 +280,7 @@ public function resolveParametrizedReferences($braceReferences, $contents, $reso * @return array * @throws XmlException */ - public function resolveEntityByNames($references) + public function resolveEntityByNames(array $references): array { $entities = []; foreach ($references as $reference) { @@ -270,10 +298,11 @@ public function resolveEntityByNames($references) * @param string $name * @return mixed * @throws XmlException + * @throws Exception */ - public function findEntity($name) + public function findEntity(string $name) { - if ($name == '_ENV' || $name == '_CREDS') { + if ($name === '_ENV' || $name === '_CREDS') { return null; } @@ -293,4 +322,20 @@ public function findEntity($name) } 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 a0651e051..479471f96 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php +++ b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php @@ -86,27 +86,38 @@ public function getTestsGroupedBySize($suiteConfiguration, $testNameToSize, $tim */ public function getTestsGroupedByFixedGroupCount($suiteConfiguration, $testNameToSize, $groupTotal) { - $suiteNameToTestSize = $this->getSuiteNameToTestSize($suiteConfiguration); - - $minRequiredGroupCount = count($suiteNameToTestSize); - if (!empty($testNameToSize)) { - $minRequiredGroupCount += 1; - } - if ($groupTotal < $minRequiredGroupCount) { - throw new FastFailException( - "Invalid parameter 'groupTotal': must be equal or greater than {$minRequiredGroupCount}" - ); - } - if (empty($suiteConfiguration)) { return $this->convertArrayIndexStartingAtOne($this->splitTestsIntoGroups($testNameToSize, $groupTotal)); } + $suiteNameToTestSize = $this->getSuiteNameToTestSize($suiteConfiguration); + // Calculate suite group totals $suiteNameToGroupCount = $this->getSuiteGroupCounts($suiteNameToTestSize, $testNameToSize, $groupTotal); - // Calculate test group total - $testGroupTotal = $groupTotal - array_sum($suiteNameToGroupCount); + $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); @@ -128,6 +139,10 @@ public function getTestsGroupedByFixedGroupCount($suiteConfiguration, $testNameT */ 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); @@ -136,9 +151,9 @@ private function getSuiteGroupCounts($suiteNameToTestSize, $testNameToSize, $gro $maxSuiteTime = max($suiteNameToSize); // Calculate 2 possible suite group times - $ceilSuiteGroupNumber = ceil($maxSuiteTime / $minGroupTime); + $ceilSuiteGroupNumber = (int)ceil($maxSuiteTime / $minGroupTime); $ceilSuiteGroupTime = max(ceil($maxSuiteTime / $ceilSuiteGroupNumber), $minGroupTime); - $floorSuiteGroupNumber = floor($maxSuiteTime / $minGroupTime); + $floorSuiteGroupNumber = (int)floor($maxSuiteTime / $minGroupTime); if ($floorSuiteGroupNumber != 0) { $floorSuiteGroupTime = max(ceil($maxSuiteTime / $floorSuiteGroupNumber), $minGroupTime); } @@ -183,6 +198,39 @@ private function getSuiteGroupCounts($suiteNameToTestSize, $testNameToSize, $gro 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. * @@ -199,7 +247,7 @@ private function getSuiteGroupCountFromGroupTime($suiteNameToTestSize, $time) if ($suiteTime <= $time) { $suiteNameToGroupCount[$suiteName] = 1; } else { - $suiteNameToGroupCount[$suiteName] = min(ceil($suiteTime/$time), $maxCount); + $suiteNameToGroupCount[$suiteName] = min((int)ceil($suiteTime/$time), $maxCount); } } return $suiteNameToGroupCount; @@ -214,17 +262,22 @@ private function getSuiteGroupCountFromGroupTime($suiteNameToTestSize, $time) */ private function splitTestsIntoGroups($tests, $groupCnt) { + if (empty($tests)) { + return []; + } + // Reverse sort the test array by size - arsort($tests); + 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) { - for ($i = 0; $i < $groupCnt; $i++) { - $sums[$i] = array_sum($groups[$i]); - } - asort($sums); // Always add the next test to the group with the smallest sum - $groups[array_key_first($sums)][$test] = $size; + $key = array_search(min($sums), $sums); + $groups[$key][$test] = $size; + $sums[$key] += $size; } // Filter empty array return array_filter($groups); @@ -434,8 +487,8 @@ private function splitTestSuite($suiteName, $tests, $maxTime) } $group = $this->createTestGroup($maxTime, $test, $size, $availableTests); - $splitSuites["{$suiteName}_${splitCount}_G"] = $group; - $this->addSuiteToConfig($suiteName, "{$suiteName}_${splitCount}_G", $group); + $splitSuites["{$suiteName}_{$splitCount}_G"] = $group; + $this->addSuiteToConfig($suiteName, "{$suiteName}_{$splitCount}_G", $group); $availableTests = array_diff_key($availableTests, $group); $splitCount++; @@ -456,7 +509,7 @@ 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; } diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index b019d5f2a..78ca97152 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -22,6 +22,7 @@ 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; @@ -30,6 +31,7 @@ use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Mustache_Engine; use Mustache_Loader_FilesystemLoader; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; /** * Class TestGenerator @@ -156,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); } @@ -207,21 +209,13 @@ private function loadAllTestObjects($testsToIgnore) * * @param string $testPhp * @param string $filename + * * @return void * @throws TestFrameworkException */ private function createCestFile(string $testPhp, string $filename) { - DirSetupUtil::createGroupDir($this->exportDirectory); - $exportFilePath = $this->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); + CestFileCreatorUtil::getInstance()->create($filename, $this->exportDirectory, $testPhp); } /** @@ -236,7 +230,7 @@ private function createCestFile(string $testPhp, string $filename) * @throws FastFailException * @throws TestReferenceException */ - 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 @@ -253,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. @@ -265,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(); @@ -278,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"; @@ -286,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; @@ -343,7 +413,6 @@ private function assembleAllTestPhp($testManifest, array $testsToIgnore) foreach ($filters as $filter) { $filter->filter($testObjects); } - foreach ($testObjects as $test) { try { // Reset flag for new test @@ -378,7 +447,7 @@ private function assembleAllTestPhp($testManifest, array $testsToIgnore) $this->debug("<comment>Finish creating test: " . $test->getCodeceptionName() . "</comment>" . PHP_EOL); // Write to manifest here if manifest is not null - if ($testManifest != null) { + if ($testManifest !== null) { $testManifest->addTest($test); } } catch (FastFailException $e) { @@ -450,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"; @@ -467,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); } @@ -493,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"; @@ -528,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"; @@ -541,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]); @@ -754,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']); @@ -778,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']; @@ -806,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); } } @@ -901,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; @@ -923,7 +1021,6 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato if (!empty($requiredEntityKeys)) { $requiredEntityKeysArray = '"' . implode('", "', $requiredEntityKeys) . '"'; } - $scope = $this->getObjectScope($generationScope); $createEntityFunctionCall = "\t\t\${$actor}->createEntity("; @@ -984,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; @@ -1019,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; } @@ -1095,6 +1192,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $parameterArray ); break; + case "grabCookieAttributes": case "grabCookie": $testSteps .= $this->wrapFunctionCallWithReturnValue( $stepKey, @@ -1160,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, @@ -1307,6 +1413,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato case "loadSessionSnapshot": case "seeInField": case "seeOptionIsSelected": + case "seeInSecretField": $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $selector, $input); break; case "seeInPageSource": @@ -1464,11 +1571,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": @@ -1511,10 +1619,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. * @@ -1594,7 +1716,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." @@ -1646,7 +1768,7 @@ private function processQuoteBreaks($match, $argument, $replacement) */ private function resolveStepKeyReferences($input, $actionGroupOrigin, $matchAll = false) { - if ($actionGroupOrigin == null) { + if ($actionGroupOrigin === null) { return $input; } $output = $input; @@ -1708,7 +1830,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; @@ -1750,6 +1872,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'; @@ -1768,9 +1894,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"; } @@ -1792,7 +1949,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 { @@ -1808,7 +1965,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'; } @@ -1818,6 +1976,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; } @@ -1955,7 +2121,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. @@ -1989,6 +2155,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. * @@ -2000,16 +2184,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; @@ -2030,17 +2205,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; @@ -2084,18 +2264,18 @@ private function resolveRuntimeReference($args, $regex, $func) foreach ($args as $key => $arg) { $newArgs[$key] = $arg; - 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; + 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; } - unset($matches); - continue; } } @@ -2163,7 +2343,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; @@ -2285,6 +2465,7 @@ private function validateXmlAttributesMutuallyExclusive($key, $tagName, $attribu 'excludes' => [ 'dontSeeCookie', 'grabCookie', + 'grabCookieAttributes', 'resetCookie', 'seeCookie', 'setCookie', 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 68642c53a..bef552da3 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php @@ -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}'"; 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 9c13eaa54..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,19 +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->setEnvironmentVariable('VERBOSE_ARTIFACTS', VERBOSE_ARTIFACTS); + $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);