diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e191adc..2dc01e00a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ Magento Functional Testing Framework Changelog ================================================ + +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 diff --git a/bin/mftf b/bin/mftf index b7d928c15..9aba5cbd0 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.12'); + $application->setVersion('2.3.13'); /** @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 4a6e604a6..f1e67abcf 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.12", + "version": "2.3.13", "license": "AGPL-3.0", "keywords": ["magento", "automation", "functional", "testing"], "config": { @@ -11,7 +11,8 @@ "require": { "php": "7.0.2|7.0.4|~7.0.6|~7.1.0|~7.2.0", "allure-framework/allure-codeception": "~1.3.0", - "codeception/codeception": "~2.3.4", + "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 4eb66074d..ce20aec87 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "de6d553c95a5721a7d4f6515c7aa15b5", + "content-hash": "e8978028c326adbd983d1a6ef7294ae0", "packages": [ { "name": "allure-framework/allure-codeception", @@ -1717,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": { @@ -1791,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", @@ -3574,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", @@ -3631,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": { @@ -3696,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", @@ -3753,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": { @@ -3805,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", @@ -3866,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": { @@ -3925,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", @@ -3979,7 +3979,7 @@ }, { "name": "symfony/finder", - "version": "v3.4.17", + "version": "v3.4.18", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -4028,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": { @@ -4078,7 +4078,7 @@ ], "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", @@ -4125,7 +4125,7 @@ }, { "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "email": "backendtea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -4258,16 +4258,16 @@ }, { "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": { @@ -4303,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", @@ -5455,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": { @@ -5515,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": { @@ -5586,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", @@ -5684,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/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 4a4a320b3..da23a20ed 100755 --- a/etc/config/codeception.dist.yml +++ b/etc/config/codeception.dist.yml @@ -12,7 +12,6 @@ settings: memory_limit: 1024M extensions: enabled: - - Codeception\Extension\RunFailed - Magento\FunctionalTestingFramework\Extension\TestContextExtension - Magento\FunctionalTestingFramework\Allure\Adapter\MagentoAllureAdapter config: diff --git a/etc/config/command.php b/etc/config/command.php index b24bafd31..600025cf4 100644 --- a/etc/config/command.php +++ b/etc/config/command.php @@ -4,34 +4,51 @@ * 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) { + exec( + escapeCommand($magentoBinary . " $command" . " $arguments") . " 2>&1", + $output, + $exitCode + ); + if ($exitCode == 0) { + http_response_code(202); + } else { + http_response_code(500); + } + echo implode("\n", $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 +72,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 dd435aee4..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\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; @@ -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/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/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/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/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 0741c48b4..16e6037fa 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php @@ -276,7 +276,7 @@ private function globRelevantPaths($testPath, $pattern) // 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 = readlink($potentialSymlink) . DIRECTORY_SEPARATOR . $pattern; + $codePath = realpath($potentialSymlink) . DIRECTORY_SEPARATOR . $pattern; } $mainModName = array_search($codePath, $allComponents) ?: basename(str_replace($pattern, '', $codePath)); @@ -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]; } @@ -554,8 +555,8 @@ private function getRegisteredModuleList() } array_walk($allComponents, function (&$value) { // Magento stores component paths with unix DIRECTORY_SEPARATOR, need to stay uniform and convert - $value .= '/Test/Mftf'; $value = realpath($value); + $value .= '/Test/Mftf'; }); return $allComponents; } catch (TestFrameworkException $e) { @@ -565,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/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":