diff --git a/CHANGELOG.md b/CHANGELOG.md index d6b01f8aa..27ec8a380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,63 @@ Magento Functional Testing Framework Changelog ================================================ +2.3.14 +----- +### Enhancements +* Maintainability + * `command.php` is now configured with an `idleTimeout` of `60` seconds, which will allow tests to continue execution if a CLI command is hanging indefinitely. + +2.3.13 +----- +### Enhancements +* Traceability + * Failed test steps are now marked with a red `x` in the generated Allure report. + * A failed `suite` `` now correctly causes subsequent tests to marked as `failed` instead of `skipped`. +* Customizability + * Added `waitForPwaElementVisible` and `waitForPwaElementNotVisible` actions. +* Modularity + * Added support for parsing of symlinked modules under `vendor`. + +### Fixes +* Fixed a PHP Fatal error that occurred if the given `MAGENTO_BASE_URL` responded with anything but a `200`. +* Fixed an issue where a test's `` would run twice with Codeception `2.4.x` +* Fixed an issue where tests using `extends` would not correctly override parent test steps +* Test actions can now send an empty string to parameterized selectors. + +### GitHub Issues/Pull requests: +* [#297](https://github.com/magento/magento2-functional-testing-framework/pull/297) -- Allow = to be part of the secret value +* [#267](https://github.com/magento/magento2-functional-testing-framework/pull/267) -- Add PHPUnit missing in dependencies +* [#266](https://github.com/magento/magento2-functional-testing-framework/pull/266) -- General refactor: ext-curl dependency + review of singletones (refactor constructs) +* [#264](https://github.com/magento/magento2-functional-testing-framework/pull/264) -- Use custom Backend domain, refactoring to Executors responsible for calling HTTP endpoints +* [#258](https://github.com/magento/magento2-functional-testing-framework/pull/258) -- Removed unused variables in FunctionCommentSniff.php +* [#256](https://github.com/magento/magento2-functional-testing-framework/pull/256) -- Removed unused variables + +2.3.12 +----- +### Enhancements +* Fetched latest allure-codeception package + +2.3.11 +----- +### Fixes +* `mftf run:failed` now correctly regenerates tests that are in suites that were parallelized (`suite` => `suite_0`, `suite_1`) + +2.3.10 +----- +### 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. +* Fixed an issue where an `element` with no `type` would cause PHP warnings during test runs. + +2.3.9 +----- +### Fixes +* Logic for parallel execution were updated to split default tests and suites from running in one group. + 2.3.8 ----- ### Fixes diff --git a/README.md b/README.md index 522ddd397..f145817be 100755 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ ## Installation -For the installation guidelines and system requirements, read [Getting Started](https://devdocs.magento.com/mftf/2.3/getting-started.html). +For the installation guidelines and system requirements, refer to [Getting Started](https://devdocs.magento.com/mftf/2.3/getting-started.html). ## Contributing We would appreciate your contributions to new components or new features, changes to the existing features, tests, documentation, specifications, bug fixes, optimizations, or just good suggestions. Report about an issue or request features opening a GitHub issue. -Learn more about contributing in our [Contribution Guidelines](https://devdocs.magento.com/mftf/2.3/contribution-guidelines.html). +Learn more about contributing in our [Contribution Guidelines](.github/CONTRIBUTING.md). If you want to participate in the documentation work, see [DevDocs Contributing](https://github.com/magento/devdocs/blob/master/.github/CONTRIBUTING.md). diff --git a/bin/mftf b/bin/mftf index d6cd5a8cd..801cdf54f 100755 --- a/bin/mftf +++ b/bin/mftf @@ -29,7 +29,7 @@ try { try { $application = new Symfony\Component\Console\Application(); $application->setName('Magento Functional Testing Framework CLI'); - $application->setVersion('2.3.8'); + $application->setVersion('2.3.14'); /** @var \Magento\FunctionalTestingFramework\Console\CommandListInterface $commandList */ $commandList = new \Magento\FunctionalTestingFramework\Console\CommandList; foreach ($commandList->getCommands() as $command) { diff --git a/composer.json b/composer.json index c59fd3279..dc6981da8 100755 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "magento/magento2-functional-testing-framework", "description": "Magento2 Functional Testing Framework", "type": "library", - "version": "2.3.8", + "version": "2.3.14", "license": "AGPL-3.0", "keywords": ["magento", "automation", "functional", "testing"], "config": { @@ -10,8 +10,9 @@ }, "require": { "php": "7.0.2|7.0.4|~7.0.6|~7.1.0|~7.2.0", - "allure-framework/allure-codeception": "~1.2.6", - "codeception/codeception": "~2.3.4", + "allure-framework/allure-codeception": "~1.3.0", + "ext-curl": "*", + "codeception/codeception": "~2.3.4 || ~2.4.0 ", "consolidation/robo": "^1.0.0", "epfremme/swagger-php": "^2.0", "flow/jsonpath": ">0.2", @@ -30,6 +31,7 @@ "goaop/framework": "2.2.0", "codacy/coverage": "^1.4", "phpmd/phpmd": "^2.6.0", + "phpunit/phpunit": "~6.5.0 || ~7.0.0", "rregeer/phpunit-coverage-check": "^0.1.4", "php-coveralls/php-coveralls": "^1.0", "symfony/stopwatch": "~3.4.6" diff --git a/composer.lock b/composer.lock index ae923f943..e55da9399 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d733d1bb277b1397e891340fed0877a2", + "content-hash": "e77971c8706a56d00fba57995a9b747d", "packages": [ { "name": "allure-framework/allure-codeception", - "version": "1.2.7", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "48598f4b4603b50b663bfe977260113a40912131" + "reference": "9d31d781b3622b028f1f6210bc76ba88438bd518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/48598f4b4603b50b663bfe977260113a40912131", - "reference": "48598f4b4603b50b663bfe977260113a40912131", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/9d31d781b3622b028f1f6210bc76ba88438bd518", + "reference": "9d31d781b3622b028f1f6210bc76ba88438bd518", "shasum": "" }, "require": { @@ -55,7 +55,7 @@ "steps", "testing" ], - "time": "2018-03-07T11:18:27+00:00" + "time": "2018-12-18T19:47:23+00:00" }, { "name": "allure-framework/allure-php-api", @@ -349,16 +349,16 @@ }, { "name": "consolidation/config", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/consolidation/config.git", - "reference": "c9fc25e9088a708637e18a256321addc0670e578" + "reference": "925231dfff32f05b787e1fddb265e789b939cf4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/config/zipball/c9fc25e9088a708637e18a256321addc0670e578", - "reference": "c9fc25e9088a708637e18a256321addc0670e578", + "url": "https://api.github.com/repos/consolidation/config/zipball/925231dfff32f05b787e1fddb265e789b939cf4c", + "reference": "925231dfff32f05b787e1fddb265e789b939cf4c", "shasum": "" }, "require": { @@ -399,7 +399,7 @@ } ], "description": "Provide configuration services for a commandline tool.", - "time": "2018-08-07T22:57:00+00:00" + "time": "2018-10-24T17:55:35+00:00" }, { "name": "consolidation/log", @@ -452,19 +452,20 @@ }, { "name": "consolidation/output-formatters", - "version": "3.2.1", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/consolidation/output-formatters.git", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5" + "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", - "reference": "d78ef59aea19d3e2e5a23f90a055155ee78a0ad5", + "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/a942680232094c4a5b21c0b7e54c20cce623ae19", + "reference": "a942680232094c4a5b21c0b7e54c20cce623ae19", "shasum": "" }, "require": { + "dflydev/dot-access-data": "^1.1.0", "php": ">=5.4.0", "symfony/console": "^2.8|^3|^4", "symfony/finder": "^2.5|^3|^4" @@ -503,7 +504,7 @@ } ], "description": "Format text by applying transformations provided by plug-in formatters.", - "time": "2018-05-25T18:02:34+00:00" + "time": "2018-10-19T22:35:38+00:00" }, { "name": "consolidation/robo", @@ -588,16 +589,16 @@ }, { "name": "consolidation/self-update", - "version": "1.1.3", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/consolidation/self-update.git", - "reference": "de33822f907e0beb0ffad24cf4b1b4fae5ada318" + "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/self-update/zipball/de33822f907e0beb0ffad24cf4b1b4fae5ada318", - "reference": "de33822f907e0beb0ffad24cf4b1b4fae5ada318", + "url": "https://api.github.com/repos/consolidation/self-update/zipball/a1c273b14ce334789825a09d06d4c87c0a02ad54", + "reference": "a1c273b14ce334789825a09d06d4c87c0a02ad54", "shasum": "" }, "require": { @@ -634,7 +635,7 @@ } ], "description": "Provides a self:update command for Symfony Console applications.", - "time": "2018-08-24T17:01:46+00:00" + "time": "2018-10-28T01:52:03+00:00" }, { "name": "container-interop/container-interop", @@ -1477,16 +1478,16 @@ }, { "name": "jms/metadata", - "version": "1.6.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/metadata.git", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", - "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", + "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", "shasum": "" }, "require": { @@ -1509,9 +1510,13 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + }, { "name": "Johannes M. Schmitt", "email": "schmittjoh@gmail.com" @@ -1524,7 +1529,7 @@ "xml", "yaml" ], - "time": "2016-12-05T10:18:33+00:00" + "time": "2018-10-26T12:40:10+00:00" }, { "name": "jms/parser-lib", @@ -1712,16 +1717,16 @@ }, { "name": "monolog/monolog", - "version": "1.23.0", + "version": "1.24.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", "shasum": "" }, "require": { @@ -1786,7 +1791,7 @@ "logging", "psr-3" ], - "time": "2017-06-19T01:22:40+00:00" + "time": "2018-11-05T09:00:11+00:00" }, { "name": "moontoast/math", @@ -3569,7 +3574,7 @@ }, { "name": "symfony/browser-kit", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", @@ -3626,16 +3631,16 @@ }, { "name": "symfony/console", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3b2b415d4c48fbefca7dc742aa0a0171bfae4e0b" + "reference": "1d228fb4602047d7b26a0554e0d3efd567da5803" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3b2b415d4c48fbefca7dc742aa0a0171bfae4e0b", - "reference": "3b2b415d4c48fbefca7dc742aa0a0171bfae4e0b", + "url": "https://api.github.com/repos/symfony/console/zipball/1d228fb4602047d7b26a0554e0d3efd567da5803", + "reference": "1d228fb4602047d7b26a0554e0d3efd567da5803", "shasum": "" }, "require": { @@ -3691,11 +3696,11 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-10-02T16:33:53+00:00" + "time": "2018-10-30T16:50:50+00:00" }, { "name": "symfony/css-selector", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -3748,16 +3753,16 @@ }, { "name": "symfony/debug", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "0a612e9dfbd2ccce03eb174365f31ecdca930ff6" + "reference": "fe9793af008b651c5441bdeab21ede8172dab097" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/0a612e9dfbd2ccce03eb174365f31ecdca930ff6", - "reference": "0a612e9dfbd2ccce03eb174365f31ecdca930ff6", + "url": "https://api.github.com/repos/symfony/debug/zipball/fe9793af008b651c5441bdeab21ede8172dab097", + "reference": "fe9793af008b651c5441bdeab21ede8172dab097", "shasum": "" }, "require": { @@ -3800,11 +3805,11 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-10-02T16:33:53+00:00" + "time": "2018-10-31T09:06:03+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", @@ -3861,16 +3866,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb" + "reference": "db9e829c8f34c3d35cf37fcd4cdb4293bc4a2f14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb", - "reference": "b2e1f19280c09a42dc64c0b72b80fe44dd6e88fb", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/db9e829c8f34c3d35cf37fcd4cdb4293bc4a2f14", + "reference": "db9e829c8f34c3d35cf37fcd4cdb4293bc4a2f14", "shasum": "" }, "require": { @@ -3920,11 +3925,11 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-07-26T09:06:28+00:00" + "time": "2018-10-30T16:50:50+00:00" }, { "name": "symfony/filesystem", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -3974,7 +3979,7 @@ }, { "name": "symfony/finder", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -4023,16 +4028,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1" + "reference": "5aea7a86ca3203dd7a257e765b4b9c9cfd01c6c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3a4498236ade473c52b92d509303e5fd1b211ab1", - "reference": "3a4498236ade473c52b92d509303e5fd1b211ab1", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5aea7a86ca3203dd7a257e765b4b9c9cfd01c6c0", + "reference": "5aea7a86ca3203dd7a257e765b4b9c9cfd01c6c0", "shasum": "" }, "require": { @@ -4073,11 +4078,11 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-10-03T08:48:18+00:00" + "time": "2018-10-31T08:57:11+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.9.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4120,7 +4125,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -4135,16 +4140,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.9.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", "shasum": "" }, "require": { @@ -4190,20 +4195,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2018-09-21T13:07:52+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.9.0", + "version": "v1.10.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "1e24b0c4a56d55aaf368763a06c6d1c7d3194934" + "reference": "6b88000cdd431cd2e940caa2cb569201f3f84224" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/1e24b0c4a56d55aaf368763a06c6d1c7d3194934", - "reference": "1e24b0c4a56d55aaf368763a06c6d1c7d3194934", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/6b88000cdd431cd2e940caa2cb569201f3f84224", + "reference": "6b88000cdd431cd2e940caa2cb569201f3f84224", "shasum": "" }, "require": { @@ -4249,20 +4254,20 @@ "portable", "shim" ], - "time": "2018-08-06T14:22:27+00:00" + "time": "2018-09-21T06:26:08+00:00" }, { "name": "symfony/process", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "1dc2977afa7d70f90f3fefbcd84152813558910e" + "reference": "35c2914a9f50519bd207164c353ae4d59182c2cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/1dc2977afa7d70f90f3fefbcd84152813558910e", - "reference": "1dc2977afa7d70f90f3fefbcd84152813558910e", + "url": "https://api.github.com/repos/symfony/process/zipball/35c2914a9f50519bd207164c353ae4d59182c2cb", + "reference": "35c2914a9f50519bd207164c353ae4d59182c2cb", "shasum": "" }, "require": { @@ -4298,11 +4303,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-10-02T12:28:39+00:00" + "time": "2018-10-14T17:33:21+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -4503,16 +4508,16 @@ "packages-dev": [ { "name": "brainmaestro/composer-git-hooks", - "version": "v2.5.0", + "version": "v2.6.0", "source": { "type": "git", "url": "https://github.com/BrainMaestro/composer-git-hooks.git", - "reference": "5b2feb35fa8d460b14fc71792aca57f97d349430" + "reference": "1ae36cc7c1a4387f026e5f085b3ba63fdf912cb7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/5b2feb35fa8d460b14fc71792aca57f97d349430", - "reference": "5b2feb35fa8d460b14fc71792aca57f97d349430", + "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/1ae36cc7c1a4387f026e5f085b3ba63fdf912cb7", + "reference": "1ae36cc7c1a4387f026e5f085b3ba63fdf912cb7", "shasum": "" }, "require": { @@ -4530,7 +4535,16 @@ "extra": { "hooks": { "pre-commit": "composer check-style", - "pre-push": "composer test" + "pre-push": [ + "composer test", + "appver=$(grep -o -P '\\d.\\d.\\d' cghooks)", + "tag=$(git tag --sort=-v:refname | head -n 1 | tr -d v)", + "if [ \"$tag\" != \"$appver\" ]; then", + "echo \"The most recent tag v$tag does not match the application version $appver\n\"", + "sed -i -E \"s/$appver/$tag/\" cghooks", + "exit 1", + "fi" + ] } }, "autoload": { @@ -4557,7 +4571,7 @@ "composer", "git" ], - "time": "2018-09-02T01:27:40+00:00" + "time": "2018-10-28T02:55:04+00:00" }, { "name": "codacy/coverage", @@ -4606,16 +4620,16 @@ }, { "name": "codeception/aspect-mock", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/Codeception/AspectMock.git", - "reference": "061386697d2f47c4d3c695e28ee23a68a2383199" + "reference": "130afd10a3d8131d267f393ee1ec322e3e583d67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/AspectMock/zipball/061386697d2f47c4d3c695e28ee23a68a2383199", - "reference": "061386697d2f47c4d3c695e28ee23a68a2383199", + "url": "https://api.github.com/repos/Codeception/AspectMock/zipball/130afd10a3d8131d267f393ee1ec322e3e583d67", + "reference": "130afd10a3d8131d267f393ee1ec322e3e583d67", "shasum": "" }, "require": { @@ -4646,7 +4660,7 @@ } ], "description": "Experimental Mocking Framework powered by Aspects", - "time": "2018-07-31T20:44:39+00:00" + "time": "2018-10-07T16:21:11+00:00" }, { "name": "doctrine/cache", @@ -5441,16 +5455,16 @@ }, { "name": "symfony/config", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "e5389132dc6320682de3643091121c048ff796b3" + "reference": "99b2fa8acc244e656cdf324ff419fbe6fd300a4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/e5389132dc6320682de3643091121c048ff796b3", - "reference": "e5389132dc6320682de3643091121c048ff796b3", + "url": "https://api.github.com/repos/symfony/config/zipball/99b2fa8acc244e656cdf324ff419fbe6fd300a4d", + "reference": "99b2fa8acc244e656cdf324ff419fbe6fd300a4d", "shasum": "" }, "require": { @@ -5501,20 +5515,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-09-08T13:15:14+00:00" + "time": "2018-10-31T09:06:03+00:00" }, { "name": "symfony/dependency-injection", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "aea20fef4e92396928b5db175788b90234c0270d" + "reference": "9c98452ac7fff4b538956775630bc9701f5384ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/aea20fef4e92396928b5db175788b90234c0270d", - "reference": "aea20fef4e92396928b5db175788b90234c0270d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9c98452ac7fff4b538956775630bc9701f5384ba", + "reference": "9c98452ac7fff4b538956775630bc9701f5384ba", "shasum": "" }, "require": { @@ -5572,11 +5586,11 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2018-10-02T12:28:39+00:00" + "time": "2018-10-31T10:49:51+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -5670,7 +5684,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "7.0.2|7.0.4|~7.0.6|~7.1.0|~7.2.0" + "php": "7.0.2|7.0.4|~7.0.6|~7.1.0|~7.2.0", + "ext-curl": "*" }, "platform-dev": [] } diff --git a/dev/tests/_bootstrap.php b/dev/tests/_bootstrap.php index b55f12c2b..4b9d04442 100644 --- a/dev/tests/_bootstrap.php +++ b/dev/tests/_bootstrap.php @@ -82,7 +82,8 @@ $paths = [ $suiteDirectory . DIRECTORY_SEPARATOR . 'functionalSuite.xml', - $suiteDirectory . DIRECTORY_SEPARATOR . 'functionalSuiteHooks.xml' + $suiteDirectory . DIRECTORY_SEPARATOR . 'functionalSuiteHooks.xml', + $suiteDirectory . DIRECTORY_SEPARATOR . 'functionalSuiteExtends.xml' ]; // create and return the iterator for these file paths diff --git a/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php b/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php index 2bd8be194..5050d5f03 100644 --- a/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php +++ b/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php @@ -68,7 +68,7 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) $phpcsFile->addError($error, $return, 'MissingReturnType'); } else { // Support both a return type and a description. - $split = preg_match('`^((?:\|?(?:array\([^\)]*\)|[\\\\a-z0-9\[\]]+))*)( .*)?`i', $content, $returnParts); + preg_match('`^((?:\|?(?:array\([^\)]*\)|[\\\\a-z0-9\[\]]+))*)( .*)?`i', $content, $returnParts); if (isset($returnParts[1]) === false) { return; } @@ -78,7 +78,7 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) // Check return type (can be multiple, separated by '|'). $typeNames = explode('|', $returnType); $suggestedNames = array(); - foreach ($typeNames as $i => $typeName) { + foreach ($typeNames as $typeName) { $suggestedName = Common::suggestType($typeName); if (in_array($suggestedName, $suggestedNames) === false) { $suggestedNames[] = $suggestedName; diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php index 8133c82a3..99125a9e3 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php @@ -85,7 +85,7 @@ private function setMockTestAndSuiteParserOutput($testData, $suiteData) $property->setValue(null); // clear suite object handler value to inject parsed content - $property = new \ReflectionProperty(SuiteObjectHandler::class, 'SUITE_OBJECT_HANLDER_INSTANCE'); + $property = new \ReflectionProperty(SuiteObjectHandler::class, 'instance'); $property->setAccessible(true); $property->setValue(null); diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php index 7bef700ab..eb6298afc 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php @@ -149,7 +149,7 @@ public function testGenerateEmptySuite() */ private function setMockTestAndSuiteParserOutput($testData, $suiteData) { - $property = new \ReflectionProperty(SuiteGenerator::class, 'SUITE_GENERATOR_INSTANCE'); + $property = new \ReflectionProperty(SuiteGenerator::class, 'instance'); $property->setAccessible(true); $property->setValue(null); @@ -159,7 +159,7 @@ private function setMockTestAndSuiteParserOutput($testData, $suiteData) $property->setValue(null); // clear suite object handler value to inject parsed content - $property = new \ReflectionProperty(SuiteObjectHandler::class, 'SUITE_OBJECT_HANLDER_INSTANCE'); + $property = new \ReflectionProperty(SuiteObjectHandler::class, 'instance'); $property->setAccessible(true); $property->setValue(null); diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php index c3d418516..b023b16c7 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; use AspectMock\Proxy\Verifier; @@ -31,6 +32,15 @@ public function setUp() TestLoggingUtil::getInstance()->setMockLoggingUtil(); } + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } + /** * Tests generating a test that extends another test * @throws \Exception @@ -38,19 +48,19 @@ public function setUp() public function testGenerateExtendedTest() { $mockActions = [ - "mockStep" => ["nodeName" => "mockNode", "stepKey" => "mockStep"] + "mockStep" => ["nodeName" => "mockNode", "stepKey" => "mockStep"] ]; $testDataArrayBuilder = new TestDataArrayBuilder(); $mockSimpleTest = $testDataArrayBuilder ->withName('simpleTest') - ->withAnnotations(['title'=>[['value' => 'simpleTest']]]) + ->withAnnotations(['title' => [['value' => 'simpleTest']]]) ->withTestActions($mockActions) ->build(); $mockExtendedTest = $testDataArrayBuilder ->withName('extendedTest') - ->withAnnotations(['title'=>[['value' => 'extendedTest']]]) + ->withAnnotations(['title' => [['value' => 'extendedTest']]]) ->withTestReference("simpleTest") ->build(); @@ -88,14 +98,14 @@ public function testGenerateExtendedWithHooks() $testDataArrayBuilder = new TestDataArrayBuilder(); $mockSimpleTest = $testDataArrayBuilder ->withName('simpleTest') - ->withAnnotations(['title'=>[['value' => 'simpleTest']]]) + ->withAnnotations(['title' => [['value' => 'simpleTest']]]) ->withBeforeHook($mockBeforeHooks) ->withAfterHook($mockAfterHooks) ->build(); $mockExtendedTest = $testDataArrayBuilder ->withName('extendedTest') - ->withAnnotations(['title'=>[['value' => 'extendedTest']]]) + ->withAnnotations(['title' => [['value' => 'extendedTest']]]) ->withTestReference("simpleTest") ->build(); @@ -117,7 +127,7 @@ public function testGenerateExtendedWithHooks() $this->assertArrayHasKey("mockStepBefore", $testObject->getHooks()['before']->getActions()); $this->assertArrayHasKey("mockStepAfter", $testObject->getHooks()['after']->getActions()); } - + /** * Tests generating a test that extends another test * @throws \Exception @@ -158,14 +168,14 @@ public function testExtendingExtendedTest() $mockSimpleTest = $testDataArrayBuilder ->withName('simpleTest') - ->withAnnotations(['title'=>[['value' => 'simpleTest']]]) + ->withAnnotations(['title' => [['value' => 'simpleTest']]]) ->withTestActions() ->withTestReference("anotherTest") ->build(); $mockExtendedTest = $testDataArrayBuilder ->withName('extendedTest') - ->withAnnotations(['title'=>[['value' => 'extendedTest']]]) + ->withAnnotations(['title' => [['value' => 'extendedTest']]]) ->withTestReference("simpleTest") ->build(); @@ -347,7 +357,7 @@ private function setMockTestOutput($testData = null, $actionGroupData = null) $property->setValue(null); // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(ActionGroupObjectHandler::class, 'ACTION_GROUP_OBJECT_HANDLER'); + $property = new \ReflectionProperty(ActionGroupObjectHandler::class, 'instance'); $property->setAccessible(true); $property->setValue(null); @@ -358,28 +368,21 @@ private function setMockTestOutput($testData = null, $actionGroupData = null) )->make(); $instance = AspectMock::double( ObjectManager::class, - ['create' => function ($clazz) use ( - $mockDataParser, - $mockActionGroupParser - ) { - if ($clazz == TestDataParser::class) { - return $mockDataParser; + [ + 'create' => function ($className) use ( + $mockDataParser, + $mockActionGroupParser + ) { + if ($className == TestDataParser::class) { + return $mockDataParser; + } + if ($className == ActionGroupDataParser::class) { + return $mockActionGroupParser; + } } - if ($clazz == ActionGroupDataParser::class) { - return $mockActionGroupParser; - } - }] + ] )->make(); // bypass the private constructor AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); } - - /** - * After class functionality - * @return void - */ - public static function tearDownAfterClass() - { - TestLoggingUtil::getInstance()->clearMockLoggingUtil(); - } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php index 356a8598e..248704adc 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php @@ -57,7 +57,13 @@ public function testGetModulePathsAlreadySet() public function testGetModulePathsAggregate() { $this->mockForceGenerate(false); - $this->setMockResolverClass(false, null, null, null, ["example" => "example" . DIRECTORY_SEPARATOR . "paths"]); + $this->setMockResolverClass( + false, + null, + null, + null, + ["Magento_example" => "example" . DIRECTORY_SEPARATOR . "paths"] + ); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties($resolver, null, [0 => "Magento_example"]); $this->assertEquals( @@ -79,7 +85,7 @@ public function testGetModulePathsLocations() $this->mockForceGenerate(false); $mockResolver = $this->setMockResolverClass( true, - [0 => "magento_example"], + [0 => "example"], null, null, ["example" => "example" . DIRECTORY_SEPARATOR . "paths"] diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php index c4a04d361..997bf0c3e 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php @@ -98,13 +98,14 @@ public function testSortWithSuites() $actualResult = $testSorter->getTestsGroupedBySize($sampleSuiteArray, $sampleTestArray, 500); // verify the resulting groups - $this->assertCount(4, $actualResult); + $this->assertCount(5, $actualResult); $expectedResults = [ - 1 => ['test3'], - 2 => ['test2','test5', 'test4'], - 3 => ['mockSuite1_0', 'test1'], - 4 => ['mockSuite1_1'] + 1 => ['mockSuite1_0'], + 2 => ['mockSuite1_1'], + 3 => ['test3'], + 4 => ['test2','test5', 'test4'], + 5 => ['test1'], ]; foreach ($actualResult as $groupNum => $group) { diff --git a/dev/tests/unit/Util/TestLoggingUtil.php b/dev/tests/unit/Util/TestLoggingUtil.php index 655048552..6d2e4b964 100644 --- a/dev/tests/unit/Util/TestLoggingUtil.php +++ b/dev/tests/unit/Util/TestLoggingUtil.php @@ -18,7 +18,7 @@ class TestLoggingUtil extends Assert /** * @var TestLoggingUtil */ - private static $INSTANCE; + private static $instance; /** * @var TestHandler @@ -40,11 +40,11 @@ private function __construct() */ public static function getInstance() { - if (self::$INSTANCE == null) { - self::$INSTANCE = new TestLoggingUtil(); + if (self::$instance == null) { + self::$instance = new TestLoggingUtil(); } - return self::$INSTANCE; + return self::$instance; } /** @@ -61,7 +61,7 @@ public function setMockLoggingUtil() LoggingUtil::class, ['getLogger' => $testLogger] )->make(); - $property = new \ReflectionProperty(LoggingUtil::class, 'INSTANCE'); + $property = new \ReflectionProperty(LoggingUtil::class, 'instance'); $property->setAccessible(true); $property->setValue($mockLoggingUtil); } diff --git a/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt b/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt index 18881c269..0ee49a80e 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt @@ -27,6 +27,6 @@ class ActionGroupWithSectionAndDataAsArgumentsCest */ public function ActionGroupWithSectionAndDataAsArguments(AcceptanceTester $I) { - $I->waitForElementVisible("#element .John"); + $I->waitForElementVisible("#element .John", 10); } } diff --git a/dev/tests/verification/Resources/BasicFunctionalTest.txt b/dev/tests/verification/Resources/BasicFunctionalTest.txt index 493d3c2c0..9970e81ec 100644 --- a/dev/tests/verification/Resources/BasicFunctionalTest.txt +++ b/dev/tests/verification/Resources/BasicFunctionalTest.txt @@ -126,7 +126,7 @@ class BasicFunctionalTestCest $I->moveMouseOver(".functionalTestSelector"); $I->openNewTab(); $I->pauseExecution(); - $I->performOn("#selector", function(\WebDriverElement $el) {return $el->isDisplayed();}); + $I->performOn("#selector", function(\WebDriverElement $el) {return $el->isDisplayed();}, 10); $I->pressKey("#page", "a"); $I->pressKey("#page", ['ctrl', 'a'],'new'); $I->pressKey("#page", ['shift', '111'],'1','x'); @@ -165,7 +165,7 @@ class BasicFunctionalTestCest $I->waitForElement(".functionalTestSelector", 30); $I->waitForElementNotVisible(".functionalTestSelector", 30); $I->waitForElementVisible(".functionalTestSelector", 30); - $I->waitForElementChange("#selector", function(\WebDriverElement $el) {return $el->isDisplayed();}); + $I->waitForElementChange("#selector", function(\WebDriverElement $el) {return $el->isDisplayed();}, 10); $I->waitForJS("someJsFunction", 30); $I->waitForText("someInput", 30, ".functionalTestSelector"); } diff --git a/dev/tests/verification/Resources/ExtendedChildTestInSuiteCest.txt b/dev/tests/verification/Resources/ExtendedChildTestInSuiteCest.txt new file mode 100644 index 000000000..da343e0e1 --- /dev/null +++ b/dev/tests/verification/Resources/ExtendedChildTestInSuiteCest.txt @@ -0,0 +1,63 @@ +amOnPage("/beforeUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::TRIVIAL) + * @Features({"TestModule"}) + * @Stories({"ExtendedChildTestInSuite"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ExtendedChildTestInSuite(AcceptanceTester $I) + { + $I->comment("Different Input"); + } +} diff --git a/dev/tests/verification/Resources/ExtendedChildTestNotInSuite.txt b/dev/tests/verification/Resources/ExtendedChildTestNotInSuite.txt new file mode 100644 index 000000000..cba6db454 --- /dev/null +++ b/dev/tests/verification/Resources/ExtendedChildTestNotInSuite.txt @@ -0,0 +1,62 @@ +amOnPage("/beforeUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->amOnPage("/afterUrl"); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); + } + + /** + * @Severity(level = SeverityLevel::TRIVIAL) + * @Features({"TestModule"}) + * @Stories({"ExtendedChildTestNotInSuite"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ExtendedChildTestNotInSuite(AcceptanceTester $I) + { + $I->comment("Different Input"); + } +} diff --git a/dev/tests/verification/Resources/SectionReplacementTest.txt b/dev/tests/verification/Resources/SectionReplacementTest.txt index 5bdba2812..c4d738811 100644 --- a/dev/tests/verification/Resources/SectionReplacementTest.txt +++ b/dev/tests/verification/Resources/SectionReplacementTest.txt @@ -68,5 +68,7 @@ class SectionReplacementTestCest $I->click("#stringLiteral1-" . PersistedObjectHandler::getInstance()->retrieveEntityField('createdData', 'firstname', 'test') . " .Doe" . msq("uniqueData")); $I->click("#element .1#element .2"); $I->click("#element .1#element .{$data}"); + $I->click("(//div[@data-role='slide'])[1]/a[@data-element='link'][contains(@href,'')]"); + $I->click("(//div[@data-role='slide'])[1]/a[@data-element='link'][contains(@href,' ')]"); } } diff --git a/dev/tests/verification/Resources/functionalSuiteHooks.txt b/dev/tests/verification/Resources/functionalSuiteHooks.txt index 665f06a05..0a6dc463f 100644 --- a/dev/tests/verification/Resources/functionalSuiteHooks.txt +++ b/dev/tests/verification/Resources/functionalSuiteHooks.txt @@ -30,7 +30,7 @@ class functionalSuiteHooks extends \Codeception\GroupObject if ($this->preconditionFailure != null) { //if our preconditions fail, we need to mark all the tests as incomplete. - $e->getTest()->getMetadata()->setIncomplete($this->preconditionFailure); + $e->getTest()->getMetadata()->setIncomplete("SUITE PRECONDITION FAILED:" . PHP_EOL . $this->preconditionFailure); } } diff --git a/dev/tests/verification/TestModule/Section/SampleSection.xml b/dev/tests/verification/TestModule/Section/SampleSection.xml index 735c5fe7b..a560e2f1e 100644 --- a/dev/tests/verification/TestModule/Section/SampleSection.xml +++ b/dev/tests/verification/TestModule/Section/SampleSection.xml @@ -18,5 +18,6 @@ + diff --git a/dev/tests/verification/TestModule/Test/ExtendedFunctionalTest.xml b/dev/tests/verification/TestModule/Test/ExtendedFunctionalTest.xml index 9d50f7197..486a6036f 100644 --- a/dev/tests/verification/TestModule/Test/ExtendedFunctionalTest.xml +++ b/dev/tests/verification/TestModule/Test/ExtendedFunctionalTest.xml @@ -158,4 +158,44 @@ + + + + + + <group value="ExtendedTestRelatedToSuite"/> + <features value="ExtendedTestRelatedToSuiteParentTest"/> + <stories value="ExtendedTestRelatedToSuiteParentTest"/> + </annotations> + <before> + <amOnPage url="/beforeUrl" stepKey="beforeAmOnPageKey"/> + </before> + <after> + <amOnPage url="/afterUrl" stepKey="afterAmOnPageKey"/> + </after> + <comment stepKey="basicCommentWithNoData" userInput="Parent Comment"/> + <amOnPage url="/url/in/parent" stepKey="amOnPageInParent"/> + </test> + + <test name="ExtendedChildTestInSuite" extends="ExtendedTestRelatedToSuiteParentTest"> + <annotations> + <severity value="MINOR"/> + <title value="ExtendedChildTestInSuite"/> + <group value="ExtendedTestInSuite"/> + <features value="ExtendedChildTestInSuite"/> + <stories value="ExtendedChildTestInSuite"/> + </annotations> + <comment stepKey="basicCommentWithNoData" userInput="Different Input"/> + <remove keyForRemoval="amOnPageInParent"/> + </test> + <test name="ExtendedChildTestNotInSuite" extends="ExtendedTestRelatedToSuiteParentTest"> + <annotations> + <severity value="MINOR"/> + <title value="ExtendedChildTestNotInSuite"/> + <features value="ExtendedChildTestNotInSuite"/> + <stories value="ExtendedChildTestNotInSuite"/> + </annotations> + <comment stepKey="basicCommentWithNoData" userInput="Different Input"/> + <remove keyForRemoval="amOnPageInParent"/> + </test> </tests> \ No newline at end of file diff --git a/dev/tests/verification/TestModule/Test/SectionReplacementTest.xml b/dev/tests/verification/TestModule/Test/SectionReplacementTest.xml index 9daef72cd..17aeb4d69 100644 --- a/dev/tests/verification/TestModule/Test/SectionReplacementTest.xml +++ b/dev/tests/verification/TestModule/Test/SectionReplacementTest.xml @@ -50,5 +50,8 @@ <click stepKey="selectorReplaceTwoParamElements" selector="{{SampleSection.oneParamElement('1')}}{{SampleSection.oneParamElement('2')}}"/> <click stepKey="selectorReplaceTwoParamMixedTypes" selector="{{SampleSection.oneParamElement('1')}}{{SampleSection.oneParamElement({$data})}}"/> + + <click stepKey="selectorParamWithEmptyString" selector="{{SampleSection.anotherTwoParamsElement('1', '')}}"/> + <click stepKey="selectorParamWithASpace" selector="{{SampleSection.anotherTwoParamsElement('1', ' ')}}"/> </test> </tests> diff --git a/dev/tests/verification/Tests/ExtendedGenerationTest.php b/dev/tests/verification/Tests/ExtendedGenerationTest.php index e7bb0a879..73fb5fdfb 100644 --- a/dev/tests/verification/Tests/ExtendedGenerationTest.php +++ b/dev/tests/verification/Tests/ExtendedGenerationTest.php @@ -118,4 +118,15 @@ public function testExtendingSkippedGeneration() { $this->generateAndCompareTest('ExtendingSkippedTest'); } + + /** + * Tests extending and removing parent steps test generation. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendingAndRemovingStepsGeneration() + { + $this->generateAndCompareTest('ExtendedChildTestNotInSuite'); + } } diff --git a/dev/tests/verification/Tests/SuiteGenerationTest.php b/dev/tests/verification/Tests/SuiteGenerationTest.php index 52b34b3ef..f305e565f 100644 --- a/dev/tests/verification/Tests/SuiteGenerationTest.php +++ b/dev/tests/verification/Tests/SuiteGenerationTest.php @@ -285,6 +285,48 @@ public function testSuiteGenerationSingleRun() $this->assertEquals($expectedManifest, file_get_contents(self::getManifestFilePath())); } + /** + * Test extends tests generation in a suite + */ + public function testSuiteGenerationWithExtends() + { + $groupName = 'suiteExtends'; + + $expectedFileNames = [ + 'ExtendedChildTestInSuiteCest' + ]; + + // Generate the Suite + SuiteGenerator::getInstance()->generateSuite($groupName); + + // Validate log message and add group name for later deletion + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + "suite generated", + ['suite' => $groupName, 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . $groupName] + ); + self::$TEST_GROUPS[] = $groupName; + + // Validate Yaml file updated + $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); + $this->assertArrayHasKey($groupName, $yml['groups']); + + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . + $groupName . + DIRECTORY_SEPARATOR; + + // Validate tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); + + foreach ($expectedFileNames as $expectedFileName) { + $this->assertTrue(in_array($expectedFileName . ".php", $dirContents)); + $this->assertFileEquals( + self::RESOURCES_PATH . DIRECTORY_SEPARATOR . $expectedFileName . ".txt", + $suiteResultBaseDir . $expectedFileName . ".php" + ); + } + } + /** * revert any changes made to config.yml * remove _generated directory diff --git a/dev/tests/verification/_suite/functionalSuiteExtends.xml b/dev/tests/verification/_suite/functionalSuiteExtends.xml new file mode 100644 index 000000000..aac3b51e5 --- /dev/null +++ b/dev/tests/verification/_suite/functionalSuiteExtends.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> + <suite name="suiteExtends"> + <include> + <group name="ExtendedTestInSuite"/> + </include> + </suite> +</suites> diff --git a/etc/config/.env.example b/etc/config/.env.example index 17ea384ca..5dc7168be 100644 --- a/etc/config/.env.example +++ b/etc/config/.env.example @@ -4,6 +4,9 @@ #*** Set the base URL for your Magento instance ***# MAGENTO_BASE_URL=http://devdocs.magento.com/ +#*** Uncomment if you are running Admin Panel on separate domain (used with MAGENTO_BACKEND_NAME) ***# +# MAGENTO_BACKEND_BASE_HOST=http://admin.example.com/ + #*** Set the Admin Username and Password for your Magento instance ***# MAGENTO_BACKEND_NAME=admin MAGENTO_ADMIN_USERNAME=admin @@ -23,8 +26,9 @@ MAGENTO_ADMIN_PASSWORD=123123q BROWSER=chrome #*** 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= -#MAGENTO_RESTAPI_SERVER_PORT= +#MAGENTO_RESTAPI_SERVER_HOST=restapi.magento.com +#MAGENTO_RESTAPI_SERVER_PORT=8080 +#MAGENTO_RESTAPI_SERVER_PROTOCOL=https #*** Uncomment these properties to set up a dev environment with symlinked projects ***# #TESTS_BP= @@ -40,4 +44,7 @@ MODULE_WHITELIST=Magento_Framework,Magento_ConfigurableProductWishlist,Magento_C #*** Bool property which allows the user to toggle debug output during test execution #MFTF_DEBUG= + +#*** Default timeout for wait actions +#WAIT_TIMEOUT=10 #*** End of .env ***# diff --git a/etc/config/codeception.dist.yml b/etc/config/codeception.dist.yml index a4ea0dcb5..da23a20ed 100755 --- a/etc/config/codeception.dist.yml +++ b/etc/config/codeception.dist.yml @@ -12,11 +12,10 @@ settings: memory_limit: 1024M extensions: enabled: - - Codeception\Extension\RunFailed - Magento\FunctionalTestingFramework\Extension\TestContextExtension - Magento\FunctionalTestingFramework\Allure\Adapter\MagentoAllureAdapter config: - Yandex\Allure\Adapter\AllureAdapter: + Magento\FunctionalTestingFramework\Allure\Adapter\MagentoAllureAdapter: deletePreviousResults: true outputDirectory: allure-results ignoredAnnotations: diff --git a/etc/config/command.php b/etc/config/command.php index b24bafd31..bc8688c21 100644 --- a/etc/config/command.php +++ b/etc/config/command.php @@ -4,34 +4,66 @@ * See COPYING.txt for license details. */ -if (isset($_POST['command'])) { +require_once __DIR__ . '/../../../../app/bootstrap.php'; + +if (!empty($_POST['token']) && !empty($_POST['command'])) { + $magentoObjectManagerFactory = \Magento\Framework\App\Bootstrap::createObjectManagerFactory(BP, $_SERVER); + $magentoObjectManager = $magentoObjectManagerFactory->create($_SERVER); + $tokenModel = $magentoObjectManager->get(\Magento\Integration\Model\Oauth\Token::class); + + $tokenPassedIn = urldecode($_POST['token']); $command = urldecode($_POST['command']); - if (array_key_exists("arguments", $_POST)) { + + if (!empty($_POST['arguments'])) { $arguments = urldecode($_POST['arguments']); } else { $arguments = null; } - $php = PHP_BINDIR ? PHP_BINDIR . '/php' : 'php'; - $valid = validateCommand($command); - if ($valid) { - exec( - escapeCommand($php . ' -f ../../../../bin/magento ' . $command) . " $arguments" ." 2>&1", - $output, - $exitCode - ); - if ($exitCode == 0) { - http_response_code(202); + + // Token returned will be null if the token we passed in is invalid + $tokenFromMagento = $tokenModel->loadByToken($tokenPassedIn)->getToken(); + if (!empty($tokenFromMagento) && ($tokenFromMagento == $tokenPassedIn)) { + $php = PHP_BINDIR ? PHP_BINDIR . '/php' : 'php'; + $magentoBinary = $php . ' -f ../../../../bin/magento'; + $valid = validateCommand($magentoBinary, $command); + if ($valid) { + $process = new Symfony\Component\Process\Process($magentoBinary . " $command" . " $arguments"); + $process->setIdleTimeout(60); + $process->setTimeout(0); + $idleTimeout = false; + try { + $process->run(); + $output = $process->getOutput(); + if (!$process->isSuccessful()) { + $output = $process->getErrorOutput(); + } + if (empty($output)) { + $output = "CLI did not return output."; + } + + } catch (Symfony\Component\Process\Exception\ProcessTimedOutException $exception) { + $output = "CLI command timed out, no output available."; + $idleTimeout = true; + } + $exitCode = $process->getExitCode(); + + if ($exitCode == 0 || $idleTimeout) { + http_response_code(202); + } else { + http_response_code(500); + } + echo $output; } else { - http_response_code(500); + http_response_code(403); + echo "Given command not found valid in Magento CLI Command list."; } - echo implode("\n", $output); } else { - http_response_code(403); - echo "Given command not found valid in Magento CLI Command list."; + http_response_code(401); + echo("Command not unauthorized."); } } else { http_response_code(412); - echo("Command parameter is not set."); + echo("Required parameters are not set."); } /** @@ -55,13 +87,13 @@ function escapeCommand($command) /** * Checks magento list of CLI commands for given $command. Does not check command parameters, just base command. + * @param string $magentoBinary * @param string $command * @return bool */ -function validateCommand($command) +function validateCommand($magentoBinary, $command) { - $php = PHP_BINDIR ? PHP_BINDIR . '/php' : 'php'; - exec($php . ' -f ../../../../bin/magento list', $commandList); + exec($magentoBinary . ' list', $commandList); // Trim list of commands after first whitespace $commandList = array_map("trimAfterWhitespace", $commandList); return in_array(trimAfterWhitespace($command), $commandList); diff --git a/etc/config/functional.suite.dist.yml b/etc/config/functional.suite.dist.yml index da05a13e3..12efe5b50 100644 --- a/etc/config/functional.suite.dist.yml +++ b/etc/config/functional.suite.dist.yml @@ -14,18 +14,13 @@ modules: - \Magento\FunctionalTestingFramework\Module\MagentoWebDriver - \Magento\FunctionalTestingFramework\Helper\Acceptance - \Magento\FunctionalTestingFramework\Helper\MagentoFakerData - - \Magento\FunctionalTestingFramework\Module\MagentoRestDriver: - url: "%MAGENTO_BASE_URL%/rest/default/V1/" - username: "%MAGENTO_ADMIN_USERNAME%" - password: "%MAGENTO_ADMIN_PASSWORD%" - depends: PhpBrowser - part: Json - \Magento\FunctionalTestingFramework\Module\MagentoSequence - \Magento\FunctionalTestingFramework\Module\MagentoAssert - Asserts config: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver: url: "%MAGENTO_BASE_URL%" + backend_url: "%MAGENTO_BACKEND_BASE_URL%" backend_name: "%MAGENTO_BACKEND_NAME%" browser: 'chrome' restart: true diff --git a/etc/di.xml b/etc/di.xml index 60faed3a0..3e6313bf3 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -8,7 +8,7 @@ <!-- Entity value gets replaced in Dom.php before reading $xml --> <!DOCTYPE config [ - <!ENTITY commonTestActions "acceptPopup|actionGroup|amOnPage|amOnUrl|amOnSubdomain|appendField|assertArrayIsSorted|assertArraySubset|assertElementContainsAttribute|attachFile|cancelPopup|checkOption|clearField|click|clickWithLeftButton|clickWithRightButton|closeAdminNotification|closeTab|comment|conditionalClick|createData|deleteData|updateData|getData|dontSee|dontSeeJsError|dontSeeCheckboxIsChecked|dontSeeCookie|dontSeeCurrentUrlEquals|dontSeeCurrentUrlMatches|dontSeeElement|dontSeeElementInDOM|dontSeeInCurrentUrl|dontSeeInField|dontSeeInFormFields|dontSeeInPageSource|dontSeeInSource|dontSeeInTitle|dontSeeLink|dontSeeOptionIsSelected|doubleClick|dragAndDrop|entity|executeJS|executeInSelenium|fillField|formatMoney|generateDate|grabAttributeFrom|grabCookie|grabFromCurrentUrl|grabMultiple|grabPageSource|grabTextFrom|grabValueFrom|loadSessionSnapshot|loginAsAdmin|magentoCLI|makeScreenshot|maximizeWindow|moveBack|moveForward|moveMouseOver|mSetLocale|mResetLocale|openNewTab|pauseExecution|parseFloat|performOn|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|waitForJS|waitForLoadingMaskToDisappear|waitForPageLoad|waitForText|assertArrayHasKey|assertArrayNotHasKey|assertArraySubset|assertContains|assertCount|assertEmpty|assertEquals|assertFalse|assertFileExists|assertFileNotExists|assertGreaterOrEquals|assertGreaterThan|assertGreaterThanOrEqual|assertInstanceOf|assertInternalType|assertIsEmpty|assertLessOrEquals|assertLessThan|assertLessThanOrEqual|assertNotContains|assertNotEmpty|assertNotEquals|assertNotInstanceOf|assertNotNull|assertNotRegExp|assertNotSame|assertNull|assertRegExp|assertSame|assertStringStartsNotWith|assertStringStartsWith|assertTrue|expectException|fail|dontSeeFullUrlEquals|dontSee|dontSeeFullUrlMatches|dontSeeInFullUrl|seeFullUrlEquals|seeFullUrlMatches|seeInFullUrl|grabFromFullUrl"> + <!ENTITY commonTestActions "acceptPopup|actionGroup|amOnPage|amOnUrl|amOnSubdomain|appendField|assertArrayIsSorted|assertArraySubset|assertElementContainsAttribute|attachFile|cancelPopup|checkOption|clearField|click|clickWithLeftButton|clickWithRightButton|closeAdminNotification|closeTab|comment|conditionalClick|createData|deleteData|updateData|getData|dontSee|dontSeeJsError|dontSeeCheckboxIsChecked|dontSeeCookie|dontSeeCurrentUrlEquals|dontSeeCurrentUrlMatches|dontSeeElement|dontSeeElementInDOM|dontSeeInCurrentUrl|dontSeeInField|dontSeeInFormFields|dontSeeInPageSource|dontSeeInSource|dontSeeInTitle|dontSeeLink|dontSeeOptionIsSelected|doubleClick|dragAndDrop|entity|executeJS|executeInSelenium|fillField|formatMoney|generateDate|grabAttributeFrom|grabCookie|grabFromCurrentUrl|grabMultiple|grabPageSource|grabTextFrom|grabValueFrom|loadSessionSnapshot|loginAsAdmin|magentoCLI|makeScreenshot|maximizeWindow|moveBack|moveForward|moveMouseOver|mSetLocale|mResetLocale|openNewTab|pauseExecution|parseFloat|performOn|pressKey|reloadPage|resetCookie|submitForm|resizeWindow|saveSessionSnapshot|scrollTo|scrollToTopOfPage|searchAndMultiSelectOption|see|seeCheckboxIsChecked|seeCookie|seeCurrentUrlEquals|seeCurrentUrlMatches|seeElement|seeElementInDOM|seeInCurrentUrl|seeInField|seeInFormFields|seeInPageSource|seeInPopup|seeInSource|seeInTitle|seeLink|seeNumberOfElements|seeOptionIsSelected|selectOption|setCookie|submitForm|switchToIFrame|switchToNextTab|switchToPreviousTab|switchToWindow|typeInPopup|uncheckOption|unselectOption|wait|waitForAjaxLoad|waitForElement|waitForElementChange|waitForElementNotVisible|waitForElementVisible|waitForPwaElementNotVisible|waitForPwaElementVisible|waitForJS|waitForLoadingMaskToDisappear|waitForPageLoad|waitForText|assertArrayHasKey|assertArrayNotHasKey|assertArraySubset|assertContains|assertCount|assertEmpty|assertEquals|assertFalse|assertFileExists|assertFileNotExists|assertGreaterOrEquals|assertGreaterThan|assertGreaterThanOrEqual|assertInstanceOf|assertInternalType|assertIsEmpty|assertLessOrEquals|assertLessThan|assertLessThanOrEqual|assertNotContains|assertNotEmpty|assertNotEquals|assertNotInstanceOf|assertNotNull|assertNotRegExp|assertNotSame|assertNull|assertRegExp|assertSame|assertStringStartsNotWith|assertStringStartsWith|assertTrue|expectException|fail|dontSeeFullUrlEquals|dontSee|dontSeeFullUrlMatches|dontSeeInFullUrl|seeFullUrlEquals|seeFullUrlMatches|seeInFullUrl|grabFromFullUrl"> ]> <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 index d23af52da..747e653e9 100644 --- a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php +++ b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php @@ -5,10 +5,13 @@ */ namespace Magento\FunctionalTestingFramework\Allure\Adapter; -use Magento\FunctionalTestingFramework\Data\Argument\Interpreter\NullType; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; -use Yandex\Allure\Adapter\AllureAdapter; +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 Codeception\Event\FailEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\StepEvent; @@ -20,7 +23,7 @@ * @package Magento\FunctionalTestingFramework\Allure */ -class MagentoAllureAdapter extends AllureAdapter +class MagentoAllureAdapter extends AllureCodeception { /** * Array of group values passed to test runner command @@ -117,4 +120,32 @@ public function stepBefore(StepEvent $stepEvent) $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) + { + 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)); + } } diff --git a/src/Magento/FunctionalTestingFramework/Console/CommandList.php b/src/Magento/FunctionalTestingFramework/Console/CommandList.php index 362b29296..ce971146e 100644 --- a/src/Magento/FunctionalTestingFramework/Console/CommandList.php +++ b/src/Magento/FunctionalTestingFramework/Console/CommandList.php @@ -35,6 +35,7 @@ public function __construct(array $commands = []) 'generate:tests' => new GenerateTestsCommand(), 'run:test' => new RunTestCommand(), 'run:group' => new RunTestGroupCommand(), + 'run:failed' => new RunTestFailedCommand(), 'setup:env' => new SetupEnvCommand(), 'upgrade:tests' => new UpgradeTestsCommand(), ] + $commands; diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php new file mode 100644 index 000000000..13f6b8ade --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php @@ -0,0 +1,201 @@ +<?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 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; + +class RunTestFailedCommand extends BaseGenerateCommand +{ + /** + * Default Test group to signify not in suite + */ + const DEFAULT_TEST_GROUP = 'default'; + + const TESTS_OUTPUT_DIR = TESTS_BP . + DIRECTORY_SEPARATOR . + "tests" . + DIRECTORY_SEPARATOR . + "_output" . + DIRECTORY_SEPARATOR; + + const TESTS_FAILED_FILE = self::TESTS_OUTPUT_DIR . "failed"; + const TESTS_RERUN_FILE = self::TESTS_OUTPUT_DIR . "rerun_tests"; + const TESTS_MANIFEST_FILE= TESTS_MODULE_PATH . + DIRECTORY_SEPARATOR . + "_generated" . + DIRECTORY_SEPARATOR . + "testManifest.txt"; + + /** + * @var array + */ + private $failedList = []; + + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('run:failed') + ->setDescription('Execute a set of tests referenced via failed file'); + + parent::configure(); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer|null|void + * @throws \Exception + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + // Create Mftf Configuration + MftfApplicationConfig::create( + false, + MftfApplicationConfig::GENERATION_PHASE, + false, + false + ); + + $testConfiguration = $this->getFailedTestList(); + + if ($testConfiguration === null) { + return null; + } + + $command = $this->getApplication()->find('generate:tests'); + $args = ['--tests' => $testConfiguration, '--remove' => true]; + + $command->run(new ArrayInput($args), $output); + + $testManifestList = $this->readTestManifestFile(); + + foreach ($testManifestList as $testCommand) { + $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional '; + $codeceptionCommand .= $testCommand; + + $process = new Process($codeceptionCommand); + $process->setWorkingDirectory(TESTS_BP); + $process->setIdleTimeout(600); + $process->setTimeout(0); + $process->run( + function ($type, $buffer) use ($output) { + $output->write($buffer); + } + ); + if (file_exists(self::TESTS_FAILED_FILE)) { + $this->failedList = array_merge( + $this->failedList, + $this->readFailedTestFile(self::TESTS_FAILED_FILE) + ); + } + } + foreach ($this->failedList as $test) { + $this->writeFailedTestToFile($test, self::TESTS_FAILED_FILE); + } + } + + /** + * Returns a json string of tests that failed on the last run + * + * @return string + */ + private function getFailedTestList() + { + $failedTestDetails = ['tests' => [], 'suites' => []]; + + if (realpath(self::TESTS_FAILED_FILE)) { + $testList = $this->readFailedTestFile(self::TESTS_FAILED_FILE); + + foreach ($testList as $test) { + if (!empty($test)) { + $this->writeFailedTestToFile($test, self::TESTS_RERUN_FILE); + $testInfo = explode(DIRECTORY_SEPARATOR, $test); + $testName = explode(":", $testInfo[count($testInfo) - 1])[1]; + $suiteName = $testInfo[count($testInfo) - 2]; + + if ($suiteName == self::DEFAULT_TEST_GROUP) { + array_push($failedTestDetails['tests'], $testName); + } else { + // Trim potential suite_parallel_0 to suite_parallel + $suiteNameArray = explode("_", $suiteName); + if (is_numeric(array_pop($suiteNameArray))) { + $suiteName = implode("_", $suiteNameArray); + } + $failedTestDetails['suites'] = array_merge_recursive( + $failedTestDetails['suites'], + [$suiteName => [$testName]] + ); + } + } + } + } + if (empty($failedTestDetails['tests']) & empty($failedTestDetails['suites'])) { + return null; + } + if (empty($failedTestDetails['tests'])) { + $failedTestDetails['tests'] = null; + } + if (empty($failedTestDetails['suites'])) { + $failedTestDetails['suites'] = null; + } + $testConfigurationJson = json_encode($failedTestDetails); + return $testConfigurationJson; + } + + /** + * Returns an array of run commands read from the manifest file created post generation + * + * @return array|boolean + */ + private function readTestManifestFile() + { + return file(self::TESTS_MANIFEST_FILE, FILE_IGNORE_NEW_LINES); + } + + /** + * Returns an array of tests read from the failed test file in _output + * + * @param string $filePath + * @return array|boolean + */ + private function readFailedTestFile($filePath) + { + return file($filePath, FILE_IGNORE_NEW_LINES); + } + + /** + * Writes the test name to a file if it does not already exist + * + * @param string $test + * @return void + */ + private 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/DataGenerator/Handlers/CredentialStore.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php index a28547d18..3f635a040 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php @@ -131,7 +131,7 @@ private function encryptCredFileContents($credContents) continue; } - list($key, $value) = explode("=", $credValue); + list($key, $value) = explode("=", $credValue, 2); if (!empty($value)) { $encryptedCreds[$key] = openssl_encrypt( $value, diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationElement.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationElement.php index 7338e52f3..cf8a64dcd 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationElement.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationElement.php @@ -64,11 +64,7 @@ public function __construct($key, $value, $type, $required, $nestedElements = [] $this->value = $value; $this->type = $type; $this->nestedElements = $nestedElements; - if ($required) { - $this->required = true; - } else { - $this->required = false; - } + $this->required = filter_var($required, FILTER_VALIDATE_BOOLEAN); $this->nestedMetadata = $nestedMetadata; } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AbstractExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AbstractExecutor.php index 6adf87892..b6c3f29ec 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AbstractExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AbstractExecutor.php @@ -14,32 +14,18 @@ abstract class AbstractExecutor implements CurlInterface { /** - * Base url. + * Returns Magento base URL. Used as a fallback for other services (eg. WebApi, Backend) * * @var string */ protected static $baseUrl = null; /** - * Resolve base url. - * - * @return void + * Returns base URL for Magento instance + * @return string */ - protected static function resolveBaseUrl() + public function getBaseUrl(): string { - - if ((getenv('MAGENTO_RESTAPI_SERVER_HOST') !== false) - && (getenv('MAGENTO_RESTAPI_SERVER_HOST') !== '') ) { - self::$baseUrl = getenv('MAGENTO_RESTAPI_SERVER_HOST'); - } else { - self::$baseUrl = getenv('MAGENTO_BASE_URL'); - } - - if ((getenv('MAGENTO_RESTAPI_SERVER_PORT') !== false) - && (getenv('MAGENTO_RESTAPI_SERVER_PORT') !== '')) { - self::$baseUrl .= ':' . getenv('MAGENTO_RESTAPI_SERVER_PORT'); - } - - self::$baseUrl = rtrim(self::$baseUrl, '/') . '/'; + return getenv('MAGENTO_BASE_URL'); } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php index cff7c04fb..1f7ae70d7 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php @@ -37,18 +37,11 @@ class AdminExecutor extends AbstractExecutor implements CurlInterface private $response; /** - * Should executor remove backend_name from api url + * Flag describes whether the request is to Magento Base URL, removes backend_name from api url * @var boolean */ private $removeBackend; - /** - * Backend url. - * - * @var string - */ - private static $adminUrl; - /** * Constructor. * @param boolean $removeBackend @@ -58,15 +51,21 @@ class AdminExecutor extends AbstractExecutor implements CurlInterface */ public function __construct($removeBackend) { - if (!isset(parent::$baseUrl)) { - parent::resolveBaseUrl(); - } - self::$adminUrl = parent::$baseUrl . getenv('MAGENTO_BACKEND_NAME') . '/'; $this->removeBackend = $removeBackend; $this->transport = new CurlTransport(); $this->authorize(); } + /** + * Returns base URL for Magento backend instance + * @return string + */ + public function getBaseUrl(): string + { + $backendHost = getenv('MAGENTO_BACKEND_BASE_URL') ?: parent::getBaseUrl(); + return $backendHost . getenv('MAGENTO_BACKEND_NAME') . '/'; + } + /** * Authorize admin on backend. * @@ -76,11 +75,11 @@ public function __construct($removeBackend) private function authorize() { // Perform GET to backend url so form_key is set - $this->transport->write(self::$adminUrl, [], CurlInterface::GET); + $this->transport->write($this->getBaseUrl(), [], CurlInterface::GET); $this->read(); // Authenticate admin user - $authUrl = self::$adminUrl . 'admin/auth/login/'; + $authUrl = $this->getBaseUrl() . 'admin/auth/login/'; $data = [ 'login[username]' => getenv('MAGENTO_ADMIN_USERNAME'), 'login[password]' => getenv('MAGENTO_ADMIN_PASSWORD'), @@ -119,10 +118,10 @@ private function setFormKey() public function write($url, $data = [], $method = CurlInterface::POST, $headers = []) { $url = ltrim($url, "/"); - $apiUrl = self::$adminUrl . $url; + $apiUrl = $this->getBaseUrl() . $url; if ($this->removeBackend) { - $apiUrl = parent::$baseUrl . $url; + $apiUrl = parent::getBaseUrl() . $url; } if ($this->formKey) { diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php index aa1706ef3..7e7485a82 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php @@ -67,9 +67,6 @@ class FrontendExecutor extends AbstractExecutor implements CurlInterface */ public function __construct($customerEmail, $customerPassWord) { - if (!isset(parent::$baseUrl)) { - parent::resolveBaseUrl(); - } $this->transport = new CurlTransport(); $this->customerEmail = $customerEmail; $this->customerPassword = $customerPassWord; @@ -84,11 +81,11 @@ public function __construct($customerEmail, $customerPassWord) */ private function authorize() { - $url = parent::$baseUrl . 'customer/account/login/'; - $this->transport->write($url); + $url = $this->getBaseUrl() . 'customer/account/login/'; + $this->transport->write($url, [], CurlInterface::GET); $this->read(); - $url = parent::$baseUrl . 'customer/account/loginPost/'; + $url = $this->getBaseUrl() . 'customer/account/loginPost/'; $data = [ 'login[username]' => $this->customerEmail, 'login[password]' => $this->customerPassword, @@ -146,7 +143,7 @@ public function write($url, $data = [], $method = CurlInterface::POST, $headers if (isset($data['customer_password'])) { unset($data['customer_password']); } - $apiUrl = parent::$baseUrl . $url; + $apiUrl = $this->getBaseUrl() . $url; if ($this->formKey) { $data['form_key'] = $this->formKey; } else { diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php index 16fef75f2..6aec3e58a 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php @@ -51,6 +51,13 @@ class WebapiExecutor extends AbstractExecutor implements CurlInterface */ private $storeCode; + /** + * Admin user auth token. + * + * @var string + */ + private $authToken; + /** * WebapiExecutor Constructor. * @@ -59,17 +66,37 @@ class WebapiExecutor extends AbstractExecutor implements CurlInterface */ public function __construct($storeCode = null) { - if (!isset(parent::$baseUrl)) { - parent::resolveBaseUrl(); - } - $this->storeCode = $storeCode; + $this->authToken = null; $this->transport = new CurlTransport(); $this->authorize(); } /** - * Returns the authorization token needed for some requests via REST call. + * Returns base URL for Magento Web API instance + * @return string + */ + public function getBaseUrl(): string + { + $baseUrl = parent::getBaseUrl(); + + $webapiHost = getenv('MAGENTO_RESTAPI_SERVER_HOST'); + $webapiPort = getenv("MAGENTO_RESTAPI_SERVER_PORT"); + $webapiProtocol = getenv("MAGENTO_RESTAPI_SERVER_PROTOCOL"); + + if ($webapiHost) { + $baseUrl = sprintf('%s://%s/', $webapiProtocol, $webapiHost); + } + + if ($webapiPort) { + $baseUrl = rtrim($baseUrl, '/') . ':' . $webapiPort . '/'; + } + + return $baseUrl; + } + + /** + * Acquire and store the authorization token needed for REST requests. * * @return void * @throws TestFrameworkException @@ -83,10 +110,8 @@ protected function authorize() ]; $this->transport->write($authUrl, json_encode($authCreds), CurlInterface::POST, $this->headers); - $this->headers = array_merge( - ['Authorization: Bearer ' . str_replace('"', "", $this->read())], - $this->headers - ); + $this->authToken = str_replace('"', "", $this->read()); + $this->headers = array_merge(['Authorization: Bearer ' . $this->authToken], $this->headers); } /** @@ -152,11 +177,22 @@ public function close() */ public function getFormattedUrl($resource) { - $urlResult = parent::$baseUrl . 'rest/'; + $urlResult = $this->getBaseUrl() . 'rest/'; if ($this->storeCode != null) { $urlResult .= $this->storeCode . "/"; } - $urlResult.= trim($resource, "/"); + $urlResult .= trim($resource, "/"); return $urlResult; } + + /** + * Return admin auth token. + * + * @throws TestFrameworkException + * @return string + */ + public function getAuthToken() + { + return $this->authToken; + } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php index ee61d8c12..9c8a9e175 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php @@ -197,15 +197,33 @@ private function resolveUrlReference($urlIn, $entityObjects) { $urlOut = $urlIn; $matchedParams = []; + // Find all the params ({}) references preg_match_all("/[{](.+?)[}]/", $urlIn, $matchedParams); if (!empty($matchedParams)) { foreach ($matchedParams[0] as $paramKey => $paramValue) { + $paramEntityParent = ""; + $matchedParent = []; + $dataItem = $matchedParams[1][$paramKey]; + // Find all the parent property (Type.key) references, assuming there will be only one + // parent property reference within one param + preg_match_all("/(.+?)\./", $dataItem, $matchedParent); + + if (!empty($matchedParent) && !empty($matchedParent[0])) { + $paramEntityParent = $matchedParent[1][0]; + $dataItem = preg_replace('/^'.$matchedParent[0][0].'/', '', $dataItem); + } + foreach ($entityObjects as $entityObject) { - $param = $entityObject->getDataByName( - $matchedParams[1][$paramKey], - EntityDataObject::CEST_UNIQUE_VALUE - ); + $param = null; + + if ($paramEntityParent === "" || $entityObject->getType() == $paramEntityParent) { + $param = $entityObject->getDataByName( + $dataItem, + EntityDataObject::CEST_UNIQUE_VALUE + ); + } + if (null !== $param) { $urlOut = str_replace($paramValue, $param, $urlOut); continue; diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php index a14d384a8..95bcd1bab 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php @@ -66,6 +66,8 @@ public function __construct($dependentEntities = null) * @return array * @throws \Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function resolveOperationDataArray($entityObject, $operationMetadata, $operation, $fromArray = false) { @@ -134,6 +136,13 @@ public function resolveOperationDataArray($entityObject, $operationMetadata, $op )); } } else { + $operationElementProperty = null; + if (strpos($operationElementType, '.') !== false) { + $operationElementComponents = explode('.', $operationElementType); + $operationElementType = $operationElementComponents[0]; + $operationElementProperty = $operationElementComponents[1]; + } + $entityNamesOfType = $entityObject->getLinkedEntitiesOfType($operationElementType); // If an element is required by metadata, but was not provided in the entity, throw an exception @@ -146,12 +155,23 @@ public function resolveOperationDataArray($entityObject, $operationMetadata, $op )); } foreach ($entityNamesOfType as $entityName) { - $operationDataSubArray = $this->resolveNonPrimitiveElement( - $entityName, - $operationElement, - $operation, - $fromArray - ); + if ($operationElementProperty === null) { + $operationDataSubArray = $this->resolveNonPrimitiveElement( + $entityName, + $operationElement, + $operation, + $fromArray + ); + } else { + $linkedEntityObj = $this->resolveLinkedEntityObject($entityName); + $operationDataSubArray = $linkedEntityObj->getDataByName($operationElementProperty, 0); + + if ($operationDataSubArray === null) { + throw new \Exception( + sprintf('Property %s not found in entity %s \n', $operationElementProperty, $entityName) + ); + } + } if ($operationElement->getType() == OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY) { $operationDataArray[$operationElement->getKey()][] = $operationDataSubArray; diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php b/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php index 4b67add0b..c1e023ef4 100644 --- a/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php +++ b/src/Magento/FunctionalTestingFramework/Exceptions/Collector/ExceptionCollector.php @@ -53,7 +53,7 @@ public function throwException() private function formatErrors($errors) { $flattenedErrors = []; - foreach ($errors as $key => $errorMsg) { + foreach ($errors as $errorMsg) { if (is_array($errorMsg)) { $flattenedErrors = array_merge($flattenedErrors, $this->formatErrors($errorMsg)); continue; diff --git a/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php index 3895d9efd..467074097 100644 --- a/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php +++ b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php @@ -16,6 +16,8 @@ class TestContextExtension extends BaseExtension { const TEST_PHASE_AFTER = "_after"; + const CODECEPT_AFTER_VERSION = "2.3.9"; + const TEST_FAILED_FILE = 'failed'; /** * Codeception Events Mapping to methods @@ -35,7 +37,8 @@ public function _initialize() Events::TEST_START => 'testStart', Events::TEST_FAIL => 'testFail', Events::STEP_AFTER => 'afterStep', - Events::TEST_END => 'testEnd' + Events::TEST_END => 'testEnd', + Events::RESULT_PRINT_AFTER => 'saveFailed' ]; self::$events = array_merge(parent::$events, $events); parent::_initialize(); @@ -67,7 +70,7 @@ public function testFail(\Codeception\Event\FailEvent $e) $this->runAfterBlock($e, $cest); } } - + /** * Codeception event listener function, triggered on test ending (naturally or by error). * @param \Codeception\Event\TestEvent $e @@ -115,13 +118,15 @@ private function runAfterBlock($e, $cest) try { $actorClass = $e->getTest()->getMetadata()->getCurrent('actor'); $I = new $actorClass($cest->getScenario()); - call_user_func(\Closure::bind( - function () use ($cest, $I) { - $cest->executeHook($I, 'after'); - }, - null, - $cest - )); + if (version_compare(\Codeception\Codecept::VERSION, TestContextExtension::CODECEPT_AFTER_VERSION, "<=")) { + call_user_func(\Closure::bind( + function () use ($cest, $I) { + $cest->executeHook($I, 'after'); + }, + null, + $cest + )); + } } catch (\Exception $e) { // Do not rethrow Exception } @@ -170,4 +175,52 @@ public function afterStep(\Codeception\Event\StepEvent $e) { ErrorLogger::getInstance()->logErrors($this->getDriver(), $e); } + + /** + * Saves failed tests from last codecept run command into a file in _output directory + * Removes file if there were no failures in last run command + * @param \Codeception\Event\PrintResultEvent $e + * @return void + */ + public function saveFailed(\Codeception\Event\PrintResultEvent $e) + { + $file = $this->getLogDir() . self::TEST_FAILED_FILE; + $result = $e->getResult(); + $output = []; + + // Remove previous file regardless if we're writing a new file + if (is_file($file)) { + unlink($file); + } + + foreach ($result->failures() as $fail) { + $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->failedTest())); + } + foreach ($result->errors() as $fail) { + $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->failedTest())); + } + foreach ($result->notImplemented() as $fail) { + $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->failedTest())); + } + + if (empty($output)) { + return; + } + + file_put_contents($file, implode("\n", $output)); + } + + /** + * Returns localized path to string, for writing failed file. + * @param string $path + * @return string + */ + protected function localizePath($path) + { + $root = realpath($this->getRootDir()) . DIRECTORY_SEPARATOR; + if (substr($path, 0, strlen($root)) == $root) { + return substr($path, strlen($root)); + } + return $path; + } } diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php new file mode 100644 index 000000000..98b13fe90 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Module; + +/** + * Class MagentoPwaActions + * + * Contains all custom PWA action functions to be used in PWA tests. + * + * @package Magento\FunctionalTestingFramework\Module + */ +class MagentoPwaWebDriver extends MagentoWebDriver +{ + /** + * Wait for a PWA Element to NOT be visible using JavaScript. + * + * @param null $selector + * @param null $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::isXPath($selector)) { + $this->waitForJS("return !document.evaluate(`$selector`, document);", $timeout); + } else { + $this->waitForJS("return !document.querySelector(`$selector`);", $timeout); + } + } + + /** + * Wait for a PWA Element to be visible using JavaScript. + * + * @param null $selector + * @param null $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::isXPath($selector)) { + $this->waitForJS("return !!document && !!document.evaluate(`$selector`, document);", $timeout); + } else { + $this->waitForJS("return !!document && !!document.querySelector(`$selector`);", $timeout); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php deleted file mode 100644 index 513d92dfb..000000000 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoRestDriver.php +++ /dev/null @@ -1,673 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\FunctionalTestingFramework\Module; - -use Codeception\Module\REST; -use Magento\FunctionalTestingFramework\Module\MagentoSequence; -use Magento\FunctionalTestingFramework\Util\ConfigSanitizerUtil; -use Flow\JSONPath; - -/** - * MagentoRestDriver module provides Magento REST WebService. - * - * This module can be used either with frameworks or PHPBrowser. - * If a framework module is connected, the testing will occur in the application directly. - * Otherwise, a PHPBrowser should be specified as a dependency to send requests and receive responses from a server. - * - * ## Configuration - * - * * url *optional* - the url of api - * - * This module requires PHPBrowser or any of Framework modules enabled. - * - * ### Example - * - * modules: - * enabled: - * - MagentoRestDriver: - * depends: PhpBrowser - * url: 'http://magento_base_url/rest/default/V1/' - * - * - * ## Public Properties - * - * * headers - array of headers going to be sent. - * * params - array of sent data - * * response - last response (string) - * - * ## Parts - * - * * Json - actions for validating Json responses (no Xml responses) - * * Xml - actions for validating XML responses (no Json responses) - * - * ## Conflicts - * - * Conflicts with SOAP module - * - */ -class MagentoRestDriver extends REST -{ - /** - * HTTP methods supported by REST. - */ - const HTTP_METHOD_GET = 'GET'; - const HTTP_METHOD_DELETE = 'DELETE'; - const HTTP_METHOD_PUT = 'PUT'; - const HTTP_METHOD_POST = 'POST'; - - /** - * Module required fields. - * - * @var array - */ - protected $requiredFields = [ - 'url', - 'username', - 'password' - ]; - - /** - * Module configurations. - * - * @var array - */ - protected $config = [ - 'url' => '', - 'username' => '', - 'password' => '' - ]; - - /** - * Admin tokens for Magento webapi access. - * - * @var array - */ - protected static $adminTokens = []; - - /** - * Before suite. - * - * @param array $settings - * @return void - */ - public function _beforeSuite($settings = []) - { - parent::_beforeSuite($settings); - if (empty($this->config['url']) || empty($this->config['username']) || empty($this->config['password'])) { - return; - } - - $this->config = ConfigSanitizerUtil::sanitizeWebDriverConfig($this->config, ['url']); - - $this->haveHttpHeader('Content-Type', 'application/json'); - $this->sendPOST( - 'integration/admin/token', - ['username' => $this->config['username'], 'password' => $this->config['password']] - ); - $token = substr($this->grabResponse(), 1, strlen($this->grabResponse())-2); - $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); - $this->haveHttpHeader('Authorization', 'Bearer ' . $token); - self::$adminTokens[$this->config['username']] = $token; - // @codingStandardsIgnoreStart - $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoSequence')->_initialize(); - // @codingStandardsIgnoreEnd - } - - /** - * After suite. - * @return void - */ - public function _afterSuite() - { - parent::_afterSuite(); - $this->deleteHeader('Authorization'); - } - - /** - * Get admin auth token by username and password. - * - * @param string $username - * @param string $password - * @param boolean $newToken - * @return string - * @part json - * @part xml - */ - public function getAdminAuthToken($username = null, $password = null, $newToken = false) - { - $username = $username !== null ? $username : $this->config['username']; - $password = $password !== null ? $password : $this->config['password']; - - // Use existing token if it exists - if (!$newToken - && (isset(self::$adminTokens[$username]) || array_key_exists($username, self::$adminTokens))) { - return self::$adminTokens[$username]; - } - $this->haveHttpHeader('Content-Type', 'application/json'); - $this->sendPOST('integration/admin/token', ['username' => $username, 'password' => $password]); - $token = substr($this->grabResponse(), 1, strlen($this->grabResponse())-2); - $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); - self::$adminTokens[$username] = $token; - return $token; - } - - /** - * Admin token authentication for a given user. - * - * @param string $username - * @param string $password - * @param boolean $newToken - * @part json - * @part xml - * @return void - */ - public function amAdminTokenAuthenticated($username = null, $password = null, $newToken = false) - { - $username = $username !== null ? $username : $this->config['username']; - $password = $password !== null ? $password : $this->config['password']; - - $this->haveHttpHeader('Content-Type', 'application/json'); - if ($newToken || !isset(self::$adminTokens[$username])) { - $this->sendPOST('integration/admin/token', ['username' => $username, 'password' => $password]); - $token = substr($this->grabResponse(), 1, strlen($this->grabResponse()) - 2); - $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); - self::$adminTokens[$username] = $token; - } - $this->amBearerAuthenticated(self::$adminTokens[$username]); - } - - /** - * Send REST API request. - * - * @param string $endpoint - * @param string $httpMethod - * @param array $params - * @param string $grabByJsonPath - * @param boolean $decode - * @return mixed - * @throws \LogicException - * @part json - * @part xml - */ - public function sendRestRequest($endpoint, $httpMethod, $params = [], $grabByJsonPath = null, $decode = true) - { - $this->amAdminTokenAuthenticated(); - switch ($httpMethod) { - case self::HTTP_METHOD_GET: - $this->sendGET($endpoint, $params); - break; - case self::HTTP_METHOD_POST: - $this->sendPOST($endpoint, $params); - break; - case self::HTTP_METHOD_PUT: - $this->sendPUT($endpoint, $params); - break; - case self::HTTP_METHOD_DELETE: - $this->sendDELETE($endpoint, $params); - break; - default: - throw new \LogicException("HTTP method '{$httpMethod}' is not supported."); - } - $this->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); - - if (!$decode && $grabByJsonPath === null) { - return $this->grabResponse(); - } elseif (!$decode) { - return $this->grabDataFromResponseByJsonPath($grabByJsonPath); - } else { - return \GuzzleHttp\json_decode($this->grabResponse()); - } - } - - /** - * Create a category in Magento. - * - * @param array $categoryData - * @return array|mixed - * @part json - * @part xml - */ - public function requireCategory($categoryData = []) - { - if (!$categoryData) { - $categoryData = $this->getCategoryApiData(); - } - $categoryData = $this->sendRestRequest( - self::$categoryEndpoint, - self::HTTP_METHOD_POST, - ['category' => $categoryData] - ); - return $categoryData; - } - - /** - * Create a simple product in Magento. - * - * @param integer $categoryId - * @param array $simpleProductData - * @return array|mixed - * @part json - * @part xml - */ - public function requireSimpleProduct($categoryId = 0, $simpleProductData = []) - { - if (!$simpleProductData) { - $simpleProductData = $this->getProductApiData('simple', $categoryId); - } - $simpleProductData = $this->sendRestRequest( - self::$productEndpoint, - self::HTTP_METHOD_POST, - ['product' => $simpleProductData] - ); - return $simpleProductData; - } - - /** - * Create a configurable product in Magento. - * - * @param integer $categoryId - * @param array $configurableProductData - * @return array|mixed - * @part json - * @part xml - */ - public function requireConfigurableProduct($categoryId = 0, $configurableProductData = []) - { - $configurableProductData = $this->getProductApiData('configurable', $categoryId, $configurableProductData); - $this->sendRestRequest( - self::$productEndpoint, - self::HTTP_METHOD_POST, - ['product' => $configurableProductData] - ); - - $attributeData = $this->getProductAttributeApiData(); - $attribute = $this->sendRestRequest( - self::$productAttributesEndpoint, - self::HTTP_METHOD_POST, - $attributeData - ); - $options = $this->sendRestRequest( - sprintf(self::$productAttributesOptionsEndpoint, $attribute->attribute_code), - self::HTTP_METHOD_GET - ); - - $attributeSetData = $this->getAssignAttributeToAttributeSetApiData($attribute->attribute_code); - $this->sendRestRequest( - self::$productAttributeSetEndpoint, - self::HTTP_METHOD_POST, - $attributeSetData - ); - - $simpleProduct1Data = $this->getProductApiData('simple', $categoryId); - array_push( - $simpleProduct1Data['custom_attributes'], - [ - 'attribute_code' => $attribute->attribute_code, - 'value' => $options[1]->value - ] - ); - $simpleProduct1Id = $this->sendRestRequest( - self::$productEndpoint, - self::HTTP_METHOD_POST, - ['product' => $simpleProduct1Data] - )->id; - - $simpleProduct2Data = $this->getProductApiData('simple', $categoryId); - array_push( - $simpleProduct2Data['custom_attributes'], - [ - 'attribute_code' => $attribute->attribute_code, - 'value' => $options[2]->value - ] - ); - $simpleProduct2Id = $this->sendRestRequest( - self::$productEndpoint, - self::HTTP_METHOD_POST, - ['product' => $simpleProduct2Data] - )->id; - - $tAttributes[] = [ - 'id' => $attribute->attribute_id, - 'code' => $attribute->attribute_code - ]; - - $tOptions = [ - intval($options[1]->value), - intval($options[2]->value) - ]; - - $configOptions = $this->getConfigurableProductOptionsApiData($tAttributes, $tOptions); - - $configurableProductData = $this->getConfigurableProductApiData( - $configOptions, - [$simpleProduct1Id, $simpleProduct2Id], - $configurableProductData - ); - - $configurableProductData = $this->sendRestRequest( - self::$productEndpoint . '/' . $configurableProductData['sku'], - self::HTTP_METHOD_PUT, - ['product' => $configurableProductData] - ); - return $configurableProductData; - } - - /** - * Create a product attribute in Magento. - * - * @param string $code - * @return array|mixed - * @part json - * @part xml - */ - public function requireProductAttribute($code = 'attribute') - { - $attributeData = $this->getProductAttributeApiData($code); - $attributeData = $this->sendRestRequest( - self::$productAttributesEndpoint, - self::HTTP_METHOD_POST, - $attributeData - ); - return $attributeData; - } - - /** - * Create a customer in Magento. - * - * @param array $customerData - * @param string $password - * @return array|mixed - * @part json - * @part xml - */ - public function requireCustomer(array $customerData = [], $password = '123123qW') - { - if (!$customerData) { - $customerData = $this->getCustomerApiData(); - } - $customerData = $this->getCustomerApiDataWithPassword($customerData, $password); - $customerData = $this->sendRestRequest( - self::$customersEndpoint, - self::HTTP_METHOD_POST, - $customerData - ); - return $customerData; - } - - /** - * Get category api data. - * - * @param array $categoryData - * @return array - * @part json - * @part xml - */ - public function getCategoryApiData($categoryData = []) - { - $faker = \Faker\Factory::create(); - $sq = sqs(); - return array_replace_recursive( - [ - 'parent_id' => '2', - 'name' => 'category' . $sq, - 'is_active' => true, - 'include_in_menu' => true, - 'available_sort_by' => ['position', 'name'], - 'custom_attributes' => [ - ['attribute_code' => 'url_key', 'value' => 'category' . $sq], - ['attribute_code' => 'description', 'value' => $faker->text(20)], - ['attribute_code' => 'meta_title', 'value' => $faker->text(20)], - ['attribute_code' => 'meta_keywords', 'value' => $faker->text(20)], - ['attribute_code' => 'meta_description', 'value' => $faker->text(20)], - ['attribute_code' => 'display_mode', 'value' => 'PRODUCTS'], - ['attribute_code' => 'landing_page', 'value' => ''], - ['attribute_code' => 'is_anchor', 'value' => '0'], - ['attribute_code' => 'custom_use_parent_settings', 'value' => '0'], - ['attribute_code' => 'custom_apply_to_products', 'value' => '0'], - ['attribute_code' => 'custom_design', 'value' => ''], - ['attribute_code' => 'page_layout', 'value' => ''], - ['attribute_code' => 'custom_design_to', 'value' => $faker->date($format = 'm/d/Y')], - ['attribute_code' => 'custom_design_from', 'value' => $faker->date($format = 'm/d/Y', 'now')] - ] - ], - $categoryData - ); - } - - /** - * Get simple product api data. - * - * @param string $type - * @param integer $categoryId - * @param array $productData - * @return array - * @part json - * @part xml - */ - public function getProductApiData($type = 'simple', $categoryId = 0, $productData = []) - { - $faker = \Faker\Factory::create(); - $sq = sqs(); - return array_replace_recursive( - [ - 'sku' => $type . '_product_sku' . $sq, - 'name' => $type . '_product' . $sq, - 'visibility' => 4, - 'type_id' => $type, - 'price' => $faker->randomFloat(2, 1), - 'status' => 1, - 'attribute_set_id' => 4, - 'extension_attributes' => [ - 'stock_item' => ['is_in_stock' => 1, 'qty' => $faker->numberBetween(100, 9000)] - ], - 'custom_attributes' => [ - ['attribute_code' => 'url_key', 'value' => $type . '_product' . $sq], - ['attribute_code' => 'tax_class_id', 'value' => 2], - ['attribute_code' => 'category_ids', 'value' => $categoryId], - ], - ], - $productData - ); - } - - /** - * Get Customer Api data. - * - * @param array $customerData - * @return array - * @part json - * @part xml - */ - public function getCustomerApiData($customerData = []) - { - $faker = \Faker\Factory::create(); - return array_replace_recursive( - [ - 'firstname' => $faker->firstName, - 'middlename' => $faker->firstName, - 'lastname' => $faker->lastName, - 'email' => $faker->email, - 'gender' => rand(0, 1), - 'group_id' => 1, - 'store_id' => 1, - 'website_id' => 1, - 'custom_attributes' => [ - [ - 'attribute_code' => 'disable_auto_group_change', - 'value' => '0', - ], - ], - ], - $customerData - ); - } - - /** - * Get customer data including password. - * - * @param array $customerData - * @param string $password - * @return array - * @part json - * @part xml - */ - public function getCustomerApiDataWithPassword($customerData = [], $password = '123123qW') - { - return ['customer' => self::getCustomerApiData($customerData), 'password' => $password]; - } - - /** - * @param string $code - * @param array $attributeData - * @return array - * @part json - * @part xml - */ - public function getProductAttributeApiData($code = 'attribute', $attributeData = []) - { - $sq = sqs(); - return array_replace_recursive( - [ - 'attribute' => [ - 'attribute_code' => $code . $sq, - 'frontend_labels' => [ - [ - 'store_id' => 0, - 'label' => $code . $sq - ], - ], - 'is_required' => false, - 'is_unique' => false, - 'is_visible' => true, - 'scope' => 'global', - 'default_value' => '', - 'frontend_input' => 'select', - 'is_visible_on_front' => true, - 'is_searchable' => true, - 'is_visible_in_advanced_search' => true, - 'is_filterable' => true, - 'is_filterable_in_search' => true, - //'is_used_in_grid' => true, - //'is_visible_in_grid' => true, - //'is_filterable_in_grid' => true, - 'used_in_product_listing' => true, - 'is_used_for_promo_rules' => true, - 'options' => [ - [ - 'label' => 'option1', - 'value' => '', - 'sort_order' => 0, - 'is_default' => true, - 'store_labels' => [ - [ - 'store_id' => 0, - 'label' => 'option1' - ], - [ - 'store_id' => 1, - 'label' => 'option1' - ] - ] - ], - [ - 'label' => 'option2', - 'value' => '', - 'sort_order' => 1, - 'is_default' => false, - 'store_labels' => [ - [ - 'store_id' => 0, - 'label' => 'option2' - ], - [ - 'store_id' => 1, - 'label' => 'option2' - ] - ] - ] - ] - ], - ], - $attributeData - ); - } - - /** - * @param array $attributes - * @param array $optionIds - * @return array - * @part json - * @part xml - */ - public function getConfigurableProductOptionsApiData($attributes, $optionIds) - { - $configurableProductOptions = []; - foreach ($attributes as $attribute) { - $attributeItem = [ - 'attribute_id' => (string)$attribute['id'], - 'label' => $attribute['code'], - 'values' => [] - ]; - foreach ($optionIds as $optionId) { - $attributeItem['values'][] = ['value_index' => $optionId]; - } - $configurableProductOptions [] = $attributeItem; - } - return $configurableProductOptions; - } - - /** - * @param array $configurableProductOptions - * @param array $childProductIds - * @param array $configurableProduct - * @param integer $categoryId - * @return array - * @part json - * @part xml - */ - public function getConfigurableProductApiData( - array $configurableProductOptions, - array $childProductIds, - array $configurableProduct = [], - int $categoryId = 0 - ) { - if (!$configurableProduct) { - $configurableProduct = $this->getProductApiData('configurable', $categoryId); - } - $configurableProduct = array_merge_recursive( - $configurableProduct, - [ - 'extension_attributes' => [ - 'configurable_product_options' => $configurableProductOptions, - 'configurable_product_links' => $childProductIds, - ], - ] - ); - return $configurableProduct; - } - - /** - * @param string $attributeCode - * @param integer $attributeSetId - * @param integer $attributeGroupId - * @return array - * @part json - * @part xml - */ - public function getAssignAttributeToAttributeSetApiData( - $attributeCode, - int $attributeSetId = 4, - int $attributeGroupId = 7 - ) { - return [ - 'attributeSetId' => $attributeSetId, - 'attributeGroupId' => $attributeGroupId, - 'attributeCode' => $attributeCode, - 'sortOrder' => 0 - ]; - } -} diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php index c64aa3721..93e6a5968 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php @@ -482,10 +482,12 @@ public function magentoCLI($command, $arguments = null) ); $apiURL = $baseUrl . '/' . ltrim(getenv('MAGENTO_CLI_COMMAND_PATH'), '/'); + $restExecutor = new WebapiExecutor(); $executor = new CurlTransport(); $executor->write( $apiURL, [ + 'token' => $restExecutor->getAuthToken(), getenv('MAGENTO_CLI_COMMAND_PARAMETER') => $command, 'arguments' => $arguments ], @@ -493,6 +495,7 @@ public function magentoCLI($command, $arguments = null) [] ); $response = $executor->read(); + $restExecutor->close(); $executor->close(); return $response; } diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php index 459f4a9ef..b63d5b49b 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php @@ -67,7 +67,7 @@ private function __construct() if (preg_match('/[^a-zA-Z0-9_]/', $elementName)) { throw new XmlException(sprintf(self::ELEMENT_NAME_ERROR_MSG, $elementName, $sectionName)); } - $elementType = $elementData[SectionObjectHandler::TYPE]; + $elementType = $elementData[SectionObjectHandler::TYPE] ?? null; $elementSelector = $elementData[SectionObjectHandler::SELECTOR] ?? null; $elementLocatorFunc = $elementData[SectionObjectHandler::LOCATOR_FUNCTION] ?? null; $elementTimeout = $elementData[SectionObjectHandler::TIMEOUT] ?? null; diff --git a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php index d4b044cf8..30a67c31d 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php @@ -23,7 +23,7 @@ class SuiteObjectHandler implements ObjectHandlerInterface * * @var SuiteObjectHandler */ - private static $SUITE_OBJECT_HANLDER_INSTANCE; + private static $instance; /** * Array of suite objects keyed by suite name. @@ -33,11 +33,19 @@ class SuiteObjectHandler implements ObjectHandlerInterface private $suiteObjects; /** - * SuiteObjectHandler constructor. + * Avoids instantiation of SuiteObjectHandler by new. + * @return void */ private function __construct() { - // empty constructor + } + + /** + * Avoids instantiation of SuiteObjectHandler by clone. + * @return void + */ + private function __clone() + { } /** @@ -46,14 +54,14 @@ private function __construct() * @return ObjectHandlerInterface * @throws XmlException */ - public static function getInstance() + public static function getInstance(): ObjectHandlerInterface { - if (self::$SUITE_OBJECT_HANLDER_INSTANCE == null) { - self::$SUITE_OBJECT_HANLDER_INSTANCE = new SuiteObjectHandler(); - self::$SUITE_OBJECT_HANLDER_INSTANCE->initSuiteData(); + if (self::$instance == null) { + self::$instance = new SuiteObjectHandler(); + self::$instance->initSuiteData(); } - return self::$SUITE_OBJECT_HANLDER_INSTANCE; + return self::$instance; } /** @@ -62,7 +70,7 @@ public static function getInstance() * @param string $objectName * @return SuiteObject */ - public function getObject($objectName) + public function getObject($objectName): SuiteObject { if (!array_key_exists($objectName, $this->suiteObjects)) { trigger_error("Suite ${objectName} is not defined.", E_USER_ERROR); @@ -75,7 +83,7 @@ public function getObject($objectName) * * @return array */ - public function getAllObjects() + public function getAllObjects(): array { return $this->suiteObjects; } @@ -85,7 +93,7 @@ public function getAllObjects() * * @return array */ - public function getAllTestReferences() + public function getAllTestReferences(): array { $testsReferencedInSuites = []; $suites = $this->getAllObjects(); diff --git a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php index 7f3efdbfc..9f0045d19 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php @@ -33,7 +33,7 @@ class SuiteGenerator * * @var SuiteGenerator */ - private static $SUITE_GENERATOR_INSTANCE; + private static $instance; /** * Group Class Generator initialized in constructor. @@ -43,28 +43,37 @@ class SuiteGenerator private $groupClassGenerator; /** - * SuiteGenerator constructor. + * Avoids instantiation of LoggingUtil by new. + * @return void */ private function __construct() { $this->groupClassGenerator = new GroupClassGenerator(); } + /** + * Avoids instantiation of SuiteGenerator by clone. + * @return void + */ + private function __clone() + { + } + /** * Singleton method which is used to retrieve the instance of the suite generator. * * @return SuiteGenerator */ - public static function getInstance() + public static function getInstance(): SuiteGenerator { - if (!self::$SUITE_GENERATOR_INSTANCE) { + if (!self::$instance) { // clear any previous configurations before any generation occurs. self::clearPreviousGroupPreconditions(); self::clearPreviousSessionConfigEntries(); - self::$SUITE_GENERATOR_INSTANCE = new SuiteGenerator(); + self::$instance = new SuiteGenerator(); } - return self::$SUITE_GENERATOR_INSTANCE; + return self::$instance; } /** diff --git a/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache b/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache index 953db9d0c..58c0cbf0f 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache +++ b/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache @@ -31,7 +31,7 @@ class {{suiteName}} extends \Codeception\GroupObject if ($this->preconditionFailure != null) { //if our preconditions fail, we need to mark all the tests as incomplete. - $e->getTest()->getMetadata()->setIncomplete($this->preconditionFailure); + $e->getTest()->getMetadata()->setIncomplete("SUITE PRECONDITION FAILED:" . PHP_EOL . $this->preconditionFailure); } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php index 994467bd3..d4a46adb9 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php @@ -27,7 +27,7 @@ class ActionGroupObjectHandler implements ObjectHandlerInterface * * @var ActionGroupObjectHandler */ - private static $ACTION_GROUP_OBJECT_HANDLER; + private static $instance; /** * Array of action groups indexed by name @@ -48,14 +48,13 @@ class ActionGroupObjectHandler implements ObjectHandlerInterface * * @return ActionGroupObjectHandler */ - public static function getInstance() + public static function getInstance(): ActionGroupObjectHandler { - if (!self::$ACTION_GROUP_OBJECT_HANDLER) { - self::$ACTION_GROUP_OBJECT_HANDLER = new ActionGroupObjectHandler(); - self::$ACTION_GROUP_OBJECT_HANDLER->initActionGroups(); + if (!self::$instance) { + self::$instance = new ActionGroupObjectHandler(); } - return self::$ACTION_GROUP_OBJECT_HANDLER; + return self::$instance; } /** @@ -64,6 +63,7 @@ public static function getInstance() private function __construct() { $this->extendUtil = new ObjectExtensionUtil(); + $this->initActionGroups(); } /** @@ -87,7 +87,7 @@ public function getObject($actionGroupName) * * @return array */ - public function getAllObjects() + public function getAllObjects(): array { foreach ($this->actionGroups as $actionGroupName => $actionGroup) { $this->actionGroups[$actionGroupName] = $this->extendActionGroup($actionGroup); @@ -125,7 +125,7 @@ private function initActionGroups() * @param ActionGroupObject $actionGroupObject * @return ActionGroupObject */ - private function extendActionGroup($actionGroupObject) + private function extendActionGroup($actionGroupObject): ActionGroupObject { if ($actionGroupObject->getParentName() !== null) { return $this->extendUtil->extendActionGroup($actionGroupObject); diff --git a/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php b/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php index 19cd025c9..1c0f7e2fc 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php @@ -92,10 +92,11 @@ public function getObject($testName) */ public function getAllObjects() { + $testObjects = []; foreach ($this->tests as $testName => $test) { - $this->tests[$testName] = $this->extendTest($test); + $testObjects[$testName] = $this->extendTest($test); } - return $this->tests; + return $testObjects; } /** @@ -110,7 +111,7 @@ public function getTestsByGroup($groupName) foreach ($this->tests as $test) { /** @var TestObject $test */ if (in_array($groupName, $test->getAnnotationByName('group'))) { - $relevantTests[$test->getName()] = $test; + $relevantTests[$test->getName()] = $this->extendTest($test); continue; } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php index c91ebaf5c..166a61889 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -68,6 +68,7 @@ class ActionObject const ACTION_ATTRIBUTE_SELECTOR = 'selector'; const ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER = '/\(.+\)/'; const ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN = '/({{[\w]+\.[\w\[\]]+}})|({{[\w]+\.[\w]+\((?(?!}}).)+\)}})/'; + const DEFAULT_WAIT_TIMEOUT = 10; /** * The unique identifier for the action @@ -154,6 +155,16 @@ public function __construct( } } + /** + * Retrieve default timeout in seconds for 'wait*' actions + * + * @return integer + */ + public static function getDefaultWaitTimeout() + { + return getenv('WAIT_TIMEOUT') ?: self::DEFAULT_WAIT_TIMEOUT; + } + /** * This function returns the string property stepKey. * @@ -271,7 +282,6 @@ public function resolveReferences() * Warns user if they are using old Assertion syntax. * * @return void - * @throws TestReferenceException */ public function trimAssertionAttributes() { @@ -691,7 +701,7 @@ private function matchParameterReferences($reference, $parameters) $resolvedParameters = []; foreach ($parameters as $parameter) { $parameter = trim($parameter); - preg_match_all("/[$'][\w\D]+[$']/", $parameter, $stringOrPersistedMatch); + preg_match_all("/[$'][\w\D]*[$']/", $parameter, $stringOrPersistedMatch); preg_match_all('/{\$[a-z][a-zA-Z\d]+}/', $parameter, $variableMatch); if (!empty($stringOrPersistedMatch[0])) { $resolvedParameters[] = ltrim(rtrim($parameter, "'"), "'"); diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/waitActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/waitActions.xsd index 5350d9d3e..70b8201a1 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/waitActions.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/waitActions.xsd @@ -20,6 +20,8 @@ <xs:element type="waitForJSType" name="waitForJS" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="waitForLoadingMaskToDisappearType" name="waitForLoadingMaskToDisappear" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="waitForPageLoadType" name="waitForPageLoad" minOccurs="0" maxOccurs="unbounded"/> + <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:choice> </xs:group> @@ -165,7 +167,7 @@ <xs:complexType name="waitForPageLoadType"> <xs:annotation> <xs:documentation> - Waits up to given time for page to have finished loading.. + Waits up to given time for page to have finished loading. </xs:documentation> </xs:annotation> <xs:simpleContent> @@ -176,6 +178,36 @@ </xs:simpleContent> </xs:complexType> + <xs:complexType name="waitForPwaElementNotVisibleType"> + <xs:annotation> + <xs:documentation> + Waits up to given time for a PWA Element to disappear from the screen using JavaScript. + </xs:documentation> + </xs:annotation> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute ref="time"/> + <xs:attribute ref="selector"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + + <xs:complexType name="waitForPwaElementVisibleType"> + <xs:annotation> + <xs:documentation> + Waits up to given time for a PWA Element to appear on the screen using JavaScript. + </xs:documentation> + </xs:annotation> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute ref="time"/> + <xs:attribute ref="selector"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:complexType name="waitForTextType"> <xs:annotation> <xs:documentation> diff --git a/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php b/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php index 78b0c29ef..7ac128f28 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php @@ -19,55 +19,63 @@ class LoggingUtil private $loggers = []; /** - * Singleton LogginUtil Instance + * Singleton LoggingUtil Instance * * @var LoggingUtil */ - private static $INSTANCE; + private static $instance; /** * Singleton accessor for instance variable * * @return LoggingUtil */ - public static function getInstance() + public static function getInstance(): LoggingUtil { - if (self::$INSTANCE == null) { - self::$INSTANCE = new LoggingUtil(); + if (self::$instance === null) { + self::$instance = new LoggingUtil(); } - return self::$INSTANCE; + return self::$instance; } /** - * Constructor for Logging Util + * Avoids instantiation of LoggingUtil by new. + * @return void */ private function __construct() { - // private constructor + } + + /** + * Avoids instantiation of LoggingUtil by clone. + * @return void + */ + private function __clone() + { } /** * Creates a new logger instances based on class name if it does not exist. If logger instance already exists, the * existing instance is simply returned. * - * @param string $clazz + * @param string $className * @return MftfLogger * @throws \Exception */ - public function getLogger($clazz) + public function getLogger($className): MftfLogger { - if ($clazz == null) { - throw new \Exception("You must pass a class to receive a logger"); + if ($className == null) { + throw new \Exception("You must pass a class name to receive a logger"); } - if (!array_key_exists($clazz, $this->loggers)) { - $logger = new MftfLogger($clazz); + if (!array_key_exists($className, $this->loggers)) { + $logger = new MftfLogger($className); $logger->pushHandler(new StreamHandler($this->getLoggingPath())); - $this->loggers[$clazz] = $logger; + $this->loggers[$className] = $logger; } - return $this->loggers[$clazz]; + return $this->loggers[$className]; } /** @@ -75,7 +83,7 @@ public function getLogger($clazz) * * @return string */ - public function getLoggingPath() + public function getLoggingPath(): string { return TESTS_BP . DIRECTORY_SEPARATOR . "mftf.log"; } diff --git a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php index cfe59bd38..16e6037fa 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php @@ -38,11 +38,6 @@ class ModuleResolver */ const REGISTRAR_CLASS = "\Magento\Framework\Component\ComponentRegistrar"; - /** - * Magento Directory Structure Name Prefix - */ - const MAGENTO_PREFIX = "Magento_"; - /** * Enabled modules. * @@ -277,8 +272,14 @@ private function globRelevantPaths($testPath, $pattern) $allComponents = $this->getRegisteredModuleList(); 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 = array_search($codePath, $allComponents) ?: basename(str_replace($pattern, '', $codePath)); - $mainModName = str_replace(self::MAGENTO_PREFIX, "", $mainModName); $modulePaths[$mainModName][] = $codePath; if (MftfApplicationConfig::getConfig()->verboseEnabled()) { @@ -340,17 +341,16 @@ private function getEnabledDirectoryPaths($enabledModules, $allModulePaths) { $enabledDirectoryPaths = []; foreach ($enabledModules as $magentoModuleName) { - // Magento_Backend -> Backend or DevDocs -> DevDocs (if whitelisted has no underscore) - $moduleShortName = explode('_', $magentoModuleName)[1] ?? $magentoModuleName; - if (!isset($this->knownDirectories[$moduleShortName]) && !isset($allModulePaths[$moduleShortName])) { + if (!isset($this->knownDirectories[$magentoModuleName]) && !isset($allModulePaths[$magentoModuleName])) { continue; - } elseif (isset($this->knownDirectories[$moduleShortName]) && !isset($allModulePaths[$moduleShortName])) { + } elseif (isset($this->knownDirectories[$magentoModuleName]) + && !isset($allModulePaths[$magentoModuleName])) { LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warn( "Known directory could not match to an existing path.", - ['knownDirectory' => $moduleShortName] + ['knownDirectory' => $magentoModuleName] ); } else { - $enabledDirectoryPaths[$moduleShortName] = $allModulePaths[$moduleShortName]; + $enabledDirectoryPaths[$magentoModuleName] = $allModulePaths[$magentoModuleName]; } } return $enabledDirectoryPaths; @@ -396,17 +396,18 @@ protected function getAdminToken() { $login = $_ENV['MAGENTO_ADMIN_USERNAME'] ?? null; $password = $_ENV['MAGENTO_ADMIN_PASSWORD'] ?? null; - if (!$login || !$password || !isset($_ENV['MAGENTO_BASE_URL'])) { + if (!$login || !$password || !$this->getBackendUrl()) { $message = "Cannot retrieve API token without credentials and base url, please fill out .env."; $context = [ "MAGENTO_BASE_URL" => getenv("MAGENTO_BASE_URL"), + "MAGENTO_BACKEND_BASE_URL" => getenv("MAGENTO_BACKEND_BASE_URL"), "MAGENTO_ADMIN_USERNAME" => getenv("MAGENTO_ADMIN_USERNAME"), "MAGENTO_ADMIN_PASSWORD" => getenv("MAGENTO_ADMIN_PASSWORD"), ]; throw new TestFrameworkException($message, $context); } - $url = ConfigSanitizerUtil::sanitizeUrl($_ENV['MAGENTO_BASE_URL']) . $this->adminTokenUrl; + $url = ConfigSanitizerUtil::sanitizeUrl($this->getBackendUrl()) . $this->adminTokenUrl; $data = [ 'username' => $login, 'password' => $password @@ -428,7 +429,7 @@ protected function getAdminToken() if ($responseCode !== 200) { if ($responseCode == 0) { - $details = "Could not find Magento Instance at given MAGENTO_BASE_URL"; + $details = "Could not find Magento Backend Instance at MAGENTO_BACKEND_BASE_URL or MAGENTO_BASE_URL"; } else { $details = $responseCode . " " . Response::$statusTexts[$responseCode]; } @@ -553,7 +554,9 @@ private function getRegisteredModuleList() $allComponents = array_merge($allComponents, $components->getPaths($componentType)); } array_walk($allComponents, function (&$value) { - $value .= DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR . 'Mftf'; + // Magento stores component paths with unix DIRECTORY_SEPARATOR, need to stay uniform and convert + $value = realpath($value); + $value .= '/Test/Mftf'; }); return $allComponents; } catch (TestFrameworkException $e) { @@ -563,4 +566,13 @@ private function getRegisteredModuleList() } return []; } + + /** + * Returns custom Backend URL if set, fallback to Magento Base URL + * @return string|null + */ + private function getBackendUrl() + { + return getenv('MAGENTO_BACKEND_BASE_URL') ?: getenv('MAGENTO_BASE_URL'); + } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php index 8654d64a7..8f4cac8a2 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php +++ b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php @@ -47,12 +47,18 @@ public function getTestsGroupedBySize($suiteConfiguration, $testNameToSize, $tim $testGroups = []; $splitSuiteNamesToTests = $this->createGroupsWithinSuites($suiteConfiguration, $time); $splitSuiteNamesToSize = $this->getSuiteToSize($splitSuiteNamesToTests); - $entriesForGeneration = array_merge($testNameToSize, $splitSuiteNamesToSize); - arsort($entriesForGeneration); + arsort($testNameToSize); + arsort($splitSuiteNamesToSize); - $testNameToSizeForUse = $entriesForGeneration; + $testNameToSizeForUse = $testNameToSize; $nodeNumber = 1; - foreach ($entriesForGeneration as $testName => $testSize) { + + foreach ($splitSuiteNamesToSize as $testName => $testSize) { + $testGroups[$nodeNumber] = [$testName => $testSize]; + $nodeNumber++; + } + + foreach ($testNameToSize as $testName => $testSize) { if (!array_key_exists($testName, $testNameToSizeForUse)) { // skip tests which have already been added to a group continue; diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index 533e09a51..abc62eb20 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -595,6 +595,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato if (isset($customActionAttributes['timeout'])) { $time = $customActionAttributes['timeout']; } + $time = $time ?? ActionObject::getDefaultWaitTimeout(); if (isset($customActionAttributes['parameterArray']) && $actionObject->getType() != 'pressKey') { // validate the param array is in the correct format @@ -1044,6 +1045,8 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato case "waitForElement": case "waitForElementVisible": case "waitForElementNotVisible": + case "waitForPwaElementVisible": + case "waitForPwaElementNotVisible": $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $selector, $time); break; case "waitForPageLoad":