diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..9aaee78fb --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# CODEOWNERS file for /docs/ folder. +# Forces a review from other writers for anything within /docs/. +/docs/ @magento/devdocs-admins diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ef21d7bde..61805aafe 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -160,7 +160,7 @@ Label| Description [create issue]: https://help.github.com/articles/creating-an-issue/ [create pr]: https://help.github.com/articles/creating-a-pull-request/ [Definition of Done]: https://devdocs.magento.com/guides/v2.2/contributor-guide/contributing_dod.html -[devdocs]: https://github.com/magento/devdocs/blob/master/.github/CONTRIBUTING.md +[devdocs]: https://github.com/magento/devdocs/blob/master/.github/CONTRIBUTING.html [existing issues]: https://github.com/magento/magento2-functional-testing-framework/issues?q=is%3Aopen+is%3Aissue [existing PRs]: https://github.com/magento/magento2-functional-testing-framework/pulls?q=is%3Aopen+is%3Apr [GitHub documentation]: https://help.github.com/articles/syncing-a-fork diff --git a/.gitignore b/.gitignore index d39929c6f..63ea3d26e 100755 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ codeception.yml dev/tests/functional/MFTF.suite.yml dev/tests/functional/_output dev/mftf.log -dev/tests/mftf.log \ No newline at end of file +dev/tests/mftf.log +dev/tests/docs/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 71b1607ee..913c74f48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ php: - 7.0 - 7.1 - 7.2 + - 7.3 install: composer install --no-interaction --prefer-source env: matrix: diff --git a/CHANGELOG.md b/CHANGELOG.md index 27ec8a380..afcb80adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,32 @@ Magento Functional Testing Framework Changelog ================================================ +2.4.0 +----- +### Enhancements +* Maintainability + * Added new `mftf static-checks` command to run new static checks against the attached test codebase + * See DevDocs for details + * Added new `mftf generate:docs` command that generates documentation about attached test codebase + * See DevDocs for details +* Traceability + * Allure reports for tests now contain collapsible sections for `actionGroup`s used in execution. + +### Fixes +* Fixed an issue where `magentoCli` would treat `argument="0"` as a null value. +* Fixed an issue where `amOnPage` and `waitForPwaElementVisible` would not utilize the `timeout` attribute correctly when MagentoPwaWebDriver is enabled. +* Fixed an issue where invalid XML characters would cause Allure to throw an exception without a resulting report. +* Fixed `codeception.dist.yml` configuration for keeping previous test run results. +* PHP Notices are no longer thrown when XML is missing non-necessary attributes. +* Removed unusable `fillSecretField` action from schema. + +### GitHub Issues/Pull requests: +* [#338](https://github.com/magento/magento2-functional-testing-framework/pull/338) -- Return exit codes of process started by 'run:test', 'run:group' or 'run:failed' command +* [#333](https://github.com/magento/magento2-functional-testing-framework/pull/333) -- Added Nginx specific settings to getting started doc +* [#332](https://github.com/magento/magento2-functional-testing-framework/pull/332) -- executeInSelenium action does not generate proper code +* [#318](https://github.com/magento/magento2-functional-testing-framework/pull/318) -- Reduce cyclomatic complexity in Problem Methods +* [#287](https://github.com/magento/magento2-functional-testing-framework/pull/287) -- Update requirements to include php7.3 support + 2.3.14 ----- ### Enhancements diff --git a/README.md b/README.md index f145817be..b95a7afc4 100755 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ ## Installation -For the installation guidelines and system requirements, refer to [Getting Started](https://devdocs.magento.com/mftf/2.3/getting-started.html). +For the installation guidelines and system requirements, refer to [Getting Started][]. ## Contributing We would appreciate your contributions to new components or new features, changes to the existing features, tests, documentation, specifications, bug fixes, optimizations, or just good suggestions. Report about an issue or request features opening a GitHub issue. -Learn more about contributing in our [Contribution Guidelines](.github/CONTRIBUTING.md). +Learn more about contributing in our [Contribution Guidelines][]. -If you want to participate in the documentation work, see [DevDocs Contributing](https://github.com/magento/devdocs/blob/master/.github/CONTRIBUTING.md). +If you want to participate in the documentation work, see [DevDocs Contributing][]. ### Labels applied by the MFTF team @@ -55,15 +55,26 @@ These labels are applied by the MFTF development team to community contributed i ## Reporting security issues -To report security vulnerabilities and other security issues in the Magento software or web sites, send an email with the report at [security@magento.com](mailto:security@magento.com). +To report security vulnerabilities and other security issues in the Magento software or web sites, send an email with the report at [security@magento.com][]. Do not report security issues using GitHub. -Be sure to encrypt your e-mail with our [encryption key](https://info2.magento.com/rs/magentoenterprise/images/security_at_magento.asc) if it includes sensitive information. -Learn more about reporting security issues [here](https://magento.com/security/reporting-magento-security-issue). +Be sure to encrypt your e-mail with our [encryption key][] if it includes sensitive information. +Learn more about reporting security issues [here][]. -Stay up-to-date on the latest security news and patches for Magento by signing up for [Security Alert Notifications](https://magento.com/security/sign-up). +Stay up-to-date on the latest security news and patches for Magento by signing up for [Security Alert Notifications][]. ## License Each Magento source file included in this distribution is licensed under AGPL 3.0. -See the license [here](LICENSE_AGPL3.txt) or contact [license@magentocommerce.com](mailto:license@magentocommerce.com) for a copy. +See the license [here][] or contact [license@magentocommerce.com][] for a copy. + + +[Getting Started]: docs/getting-started.md +[Contribution Guidelines]: .github/CONTRIBUTING.html +[DevDocs Contributing]: https://github.com/magento/devdocs/blob/master/.github/CONTRIBUTING.md +[security@magento.com]: mailto:security@magento.com +[encryption key]: https://info2.magento.com/rs/magentoenterprise/images/security_at_magento.asc +[here]: https://magento.com/security/reporting-magento-security-issue +[Security Alert Notifications]: https://magento.com/security/sign-up +[here]: LICENSE_AGPL3.txt +[license@magentocommerce.com]: mailto:license@magentocommerce.com \ No newline at end of file diff --git a/bin/mftf b/bin/mftf index 801cdf54f..bebc55b94 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.14'); + $application->setVersion('2.4.0'); /** @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 dc6981da8..21a2670c0 100755 --- a/composer.json +++ b/composer.json @@ -2,14 +2,14 @@ "name": "magento/magento2-functional-testing-framework", "description": "Magento2 Functional Testing Framework", "type": "library", - "version": "2.3.14", + "version": "2.4.0", "license": "AGPL-3.0", "keywords": ["magento", "automation", "functional", "testing"], "config": { "sort-packages": true }, "require": { - "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||~7.3.0", "allure-framework/allure-codeception": "~1.3.0", "ext-curl": "*", "codeception/codeception": "~2.3.4 || ~2.4.0 ", @@ -25,7 +25,7 @@ "require-dev": { "squizlabs/php_codesniffer": "~3.2", "sebastian/phpcpd": "~3.0 || ~4.0", - "brainmaestro/composer-git-hooks": "^2.3", + "brainmaestro/composer-git-hooks": "^2.3.1", "doctrine/cache": "<1.7.0", "codeception/aspect-mock": "^3.0", "goaop/framework": "2.2.0", diff --git a/composer.lock b/composer.lock index e55da9399..2e539fcf2 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": "e77971c8706a56d00fba57995a9b747d", + "content-hash": "4ae7e6856deb8d93cae8f3d7c2fbb3c0", "packages": [ { "name": "allure-framework/allure-codeception", @@ -2783,6 +2783,7 @@ "mock", "xunit" ], + "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, { @@ -4125,7 +4126,7 @@ }, { "name": "Gert de Pagter", - "email": "backendtea@gmail.com" + "email": "BackEndTea@gmail.com" } ], "description": "Symfony polyfill for ctype functions", @@ -4508,20 +4509,20 @@ "packages-dev": [ { "name": "brainmaestro/composer-git-hooks", - "version": "v2.6.0", + "version": "v2.6.1", "source": { "type": "git", "url": "https://github.com/BrainMaestro/composer-git-hooks.git", - "reference": "1ae36cc7c1a4387f026e5f085b3ba63fdf912cb7" + "reference": "137dd2aec2be494918f8bdfb18f57b55ff20015e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/1ae36cc7c1a4387f026e5f085b3ba63fdf912cb7", - "reference": "1ae36cc7c1a4387f026e5f085b3ba63fdf912cb7", + "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/137dd2aec2be494918f8bdfb18f57b55ff20015e", + "reference": "137dd2aec2be494918f8bdfb18f57b55ff20015e", "shasum": "" }, "require": { - "php": "^5.6 || >=7.0 <7.3", + "php": "^5.6 || >=7.0", "symfony/console": "^3.2 || ^4.0" }, "require-dev": { @@ -4571,7 +4572,7 @@ "composer", "git" ], - "time": "2018-10-28T02:55:04+00:00" + "time": "2018-12-28T14:57:06+00:00" }, { "name": "codacy/coverage", @@ -5684,7 +5685,7 @@ "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||~7.3.0", "ext-curl": "*" }, "platform-dev": [] diff --git a/dev/tests/_bootstrap.php b/dev/tests/_bootstrap.php index 4b9d04442..31b4462f6 100644 --- a/dev/tests/_bootstrap.php +++ b/dev/tests/_bootstrap.php @@ -60,6 +60,28 @@ defined('TESTS_BP') || define('TESTS_BP', __DIR__); defined('TESTS_MODULE_PATH') || define('TESTS_MODULE_PATH', TESTS_BP . $RELATIVE_TESTS_MODULE_PATH); defined('MAGENTO_BP') || define('MAGENTO_BP', __DIR__); +define('DOCS_OUTPUT_DIR', + FW_BP . + DIRECTORY_SEPARATOR . + "dev" . + DIRECTORY_SEPARATOR . + "tests" . + DIRECTORY_SEPARATOR . + "unit" . + DIRECTORY_SEPARATOR . + "_output" +); +define('RESOURCE_DIR', + FW_BP . + DIRECTORY_SEPARATOR . + "dev" . + DIRECTORY_SEPARATOR . + "tests" . + DIRECTORY_SEPARATOR . + "unit" . + DIRECTORY_SEPARATOR . + "Resources" +); $utilDir = DIRECTORY_SEPARATOR . 'Util'. DIRECTORY_SEPARATOR . '*.php'; diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php new file mode 100644 index 000000000..d56608894 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php @@ -0,0 +1,104 @@ +setMockLoggingUtil(); + } + + /** + * Annotation extractor takes in raw array and condenses it to expected format + * + * @throws \Exception + */ + public function testActionGroupExtractAnnotations() + { + // Test Data + $actionGroupAnnotations = [ + "nodeName" => "annotations", + "description" => [ + "nodeName" => "description", + "value" => "someDescription" + ], + "page" => [ + "nodeName" => "page", + "value" => "somePage" + ] + ]; + // Perform Test + $extractor = new ActionGroupAnnotationExtractor(); + $returnedAnnotations = $extractor->extractAnnotations($actionGroupAnnotations, "fileName"); + + // Asserts + + $this->assertEquals("somePage", $returnedAnnotations['page']); + $this->assertEquals("someDescription", $returnedAnnotations['description']); + } + + /** + * Annotation extractor should throw warning when required annotations are missing + * + * @throws \Exception + */ + public function testActionGroupMissingAnnotations() + { + // Action Group Data, missing page and description + $testAnnotations = []; + // Perform Test + $extractor = new ActionGroupAnnotationExtractor(); + AspectMock::double($extractor, ['isCommandDefined' => true]); + $extractor->extractAnnotations($testAnnotations, "fileName"); + + // Asserts + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'warning', + 'DEPRECATION: Action Group File fileName is missing required annotations.', + [ + 'actionGroup' => 'fileName', + 'missingAnnotations' => "description, page" + ] + ); + } + + /** + * Annotation extractor should not throw warning when required + * annotations are missing if command is not generate:docs + * + * @throws \Exception + */ + public function testActionGroupMissingAnnotationsNoWarning() + { + // Action Group Data, missing page and description + $testAnnotations = []; + // Perform Test + $extractor = new ActionGroupAnnotationExtractor(); + $extractor->extractAnnotations($testAnnotations, "fileName"); + + // Asserts + TestLoggingUtil::getInstance()->validateMockLogEmpty(); + } + + /** + * After class functionality + * @return void + */ + public static function tearDownAfterClass() + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/DocGeneratorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/DocGeneratorTest.php new file mode 100644 index 000000000..ae61f37af --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/DocGeneratorTest.php @@ -0,0 +1,131 @@ + "somePage", + "description" => "someDescription" + ]; + $actionGroupUnderTest = (new ActionGroupObjectBuilder()) + ->withAnnotations($annotations) + ->withFilename("filename") + ->build(); + $docGenerator = new DocGenerator(); + $docGenerator->createDocumentation( + [$actionGroupUnderTest->getName() => $actionGroupUnderTest], + DOCS_OUTPUT_DIR, + true + ); + + $docFile = DOCS_OUTPUT_DIR . DIRECTORY_SEPARATOR . self::DOC_FILENAME . ".md"; + + $this->assertTrue(file_exists($docFile)); + + $this->assertFileEquals( + RESOURCE_DIR . DIRECTORY_SEPARATOR . "basicDocumentation.txt", + $docFile + ); + } + + /** + * Test to check creation of documentation when overwriting previous + * + * @throws \Exception + */ + public function testCreateDocumentationWithOverwrite() + { + $annotations = [ + "page" => "somePage", + "description" => "someDescription" + ]; + $actionGroupUnderTest = (new ActionGroupObjectBuilder()) + ->withAnnotations($annotations) + ->withFilename("filename") + ->build(); + $docGenerator = new DocGenerator(); + $docGenerator->createDocumentation( + [$actionGroupUnderTest->getName() => $actionGroupUnderTest], + DOCS_OUTPUT_DIR, + true + ); + + $annotations = [ + "page" => "alteredPage", + "description" => "alteredDescription" + ]; + $actionGroupUnderTest = (new ActionGroupObjectBuilder()) + ->withAnnotations($annotations) + ->withFilename("filename") + ->build(); + $docGenerator = new DocGenerator(); + $docGenerator->createDocumentation( + [$actionGroupUnderTest->getName() => $actionGroupUnderTest], + DOCS_OUTPUT_DIR, + true + ); + + $docFile = DOCS_OUTPUT_DIR . DIRECTORY_SEPARATOR . self::DOC_FILENAME . ".md"; + + $this->assertTrue(file_exists($docFile)); + + $this->assertFileEquals( + RESOURCE_DIR . DIRECTORY_SEPARATOR . "alteredDocumentation.txt", + $docFile + ); + } + + /** + * Test for existing documentation without clean + * + * @throws \Exception + */ + public function testCreateDocumentationNotCleanException() + { + $annotations = [ + "page" => "somePage", + "description" => "someDescription" + ]; + $actionGroupUnderTest = (new ActionGroupObjectBuilder()) + ->withAnnotations($annotations) + ->withFilename("filename") + ->build(); + $docGenerator = new DocGenerator(); + $docGenerator->createDocumentation( + [$actionGroupUnderTest->getName() => $actionGroupUnderTest], + DOCS_OUTPUT_DIR, + true + ); + + $docFile = DOCS_OUTPUT_DIR . DIRECTORY_SEPARATOR . self::DOC_FILENAME . ".md"; + + $this->expectException(TestFrameworkException::class); + $this->expectExceptionMessage("$docFile already exists, please add --clean if you want to overwrite it."); + + $docGenerator = new DocGenerator(); + $docGenerator->createDocumentation( + [$actionGroupUnderTest->getName() => $actionGroupUnderTest], + DOCS_OUTPUT_DIR, + false + ); + } +} diff --git a/dev/tests/unit/Resources/alteredDocumentation.txt b/dev/tests/unit/Resources/alteredDocumentation.txt new file mode 100644 index 000000000..b55c30d6b --- /dev/null +++ b/dev/tests/unit/Resources/alteredDocumentation.txt @@ -0,0 +1,16 @@ +#Action Group Information +This documentation contains a list of all action groups on the pages on which they start + +##List of Pages +- [ alteredPage ](#alteredPage) +--- + +##alteredPage + +###testActionGroupObject +alteredDescription + +Located in: + +- filename +*** diff --git a/dev/tests/unit/Resources/basicDocumentation.txt b/dev/tests/unit/Resources/basicDocumentation.txt new file mode 100644 index 000000000..26a7fe719 --- /dev/null +++ b/dev/tests/unit/Resources/basicDocumentation.txt @@ -0,0 +1,16 @@ +#Action Group Information +This documentation contains a list of all action groups on the pages on which they start + +##List of Pages +- [ somePage ](#somePage) +--- + +##somePage + +###testActionGroupObject +someDescription + +Located in: + +- filename +*** diff --git a/dev/tests/unit/Util/ActionGroupObjectBuilder.php b/dev/tests/unit/Util/ActionGroupObjectBuilder.php index f97d0465d..83537ab78 100644 --- a/dev/tests/unit/Util/ActionGroupObjectBuilder.php +++ b/dev/tests/unit/Util/ActionGroupObjectBuilder.php @@ -27,6 +27,13 @@ class ActionGroupObjectBuilder */ private $actionObjects = []; + /** + * Action Group Object Builder default entity annotations. + * + * @var array + */ + private $annotations = []; + /** * Action Group Object Builder default entity arguments. * @@ -41,6 +48,13 @@ class ActionGroupObjectBuilder */ private $extends = null; + /** + * Action Group Object Builder default filenames + * + * @var string + */ + private $filename = []; + /** * Setter for the Action Group Object name * @@ -53,6 +67,18 @@ public function withName($name) return $this; } + /** + * Setter for the Action Group Object annotations + * + * @param array $annotations + * @return ActionGroupObjectBuilder + */ + public function withAnnotations($annotations) + { + $this->annotations = $annotations; + return $this; + } + /** * Setter for the Action Group Object arguments * @@ -89,6 +115,18 @@ public function withExtendedAction($extendedActionGroup) return $this; } + /** + * Setter for the Action Group Object filename + * + * @param string $filename + * @return ActionGroupObjectBuilder + */ + public function withFilename($filename) + { + $this->filename = $filename; + return $this; + } + /** * ActionGroupObjectBuilder constructor. */ @@ -108,9 +146,11 @@ public function build() { return new ActionGroupObject( $this->name, + $this->annotations, $this->arguments, $this->actionObjects, - $this->extends + $this->extends, + $this->filename ); } } diff --git a/dev/tests/unit/Util/MagentoTestCase.php b/dev/tests/unit/Util/MagentoTestCase.php index 0aa153106..6a4e5d35b 100644 --- a/dev/tests/unit/Util/MagentoTestCase.php +++ b/dev/tests/unit/Util/MagentoTestCase.php @@ -14,6 +14,14 @@ */ class MagentoTestCase extends TestCase { + public static function setUpBeforeClass() + { + if (!self::fileExists(DOCS_OUTPUT_DIR)) { + mkdir(DOCS_OUTPUT_DIR, 0755, true); + } + parent::setUpBeforeClass(); + } + /** * Teardown for removing AspectMock Double References * @return void @@ -21,5 +29,9 @@ class MagentoTestCase extends TestCase public static function tearDownAfterClass() { AspectMock::clean(); + array_map('unlink', glob(DOCS_OUTPUT_DIR . DIRECTORY_SEPARATOR . "*")); + if (file_exists(DOCS_OUTPUT_DIR)) { + rmdir(DOCS_OUTPUT_DIR); + } } } diff --git a/dev/tests/unit/Util/TestLoggingUtil.php b/dev/tests/unit/Util/TestLoggingUtil.php index 6d2e4b964..7becc609a 100644 --- a/dev/tests/unit/Util/TestLoggingUtil.php +++ b/dev/tests/unit/Util/TestLoggingUtil.php @@ -10,7 +10,6 @@ use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Logger\MftfLogger; use Monolog\Handler\TestHandler; -use Monolog\Logger; use PHPUnit\Framework\Assert; class TestLoggingUtil extends Assert @@ -66,6 +65,12 @@ public function setMockLoggingUtil() $property->setValue($mockLoggingUtil); } + public function validateMockLogEmpty() + { + $records = $this->testLogHandler->getRecords(); + $this->assertTrue(empty($records)); + } + /** * Function which validates messages have been logged as intended during test execution. * diff --git a/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt b/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt index 8d33003af..a9d6c3d27 100644 --- a/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt +++ b/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt @@ -24,7 +24,9 @@ class ActionGroupContainsStepKeyInArgTextCest */ public function _before(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupContainsStepKeyInArgValue (actionGroup)"); $I->see("arg1", ".selector"); + $I->comment("Exiting Action Group actionGroupContainsStepKeyInArgValue (actionGroup)"); } /** @@ -36,6 +38,8 @@ class ActionGroupContainsStepKeyInArgTextCest */ public function ActionGroupContainsStepKeyInArgText(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupContainsStepKeyInArgValue (actionGroup)"); $I->see("arg1", ".selector"); + $I->comment("Exiting Action Group actionGroupContainsStepKeyInArgValue (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt b/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt index 2c2536136..1573c0d43 100644 --- a/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt +++ b/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt @@ -27,11 +27,13 @@ class ActionGroupMergedViaInsertAfterCest */ public function ActionGroupMergedViaInsertAfter(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroupForMassMergeAfter (keyone)"); $I->fillField("#foo", "foo"); $I->fillField("#bar", "bar"); $I->click("#foo2"); $I->click("#bar2"); $I->click("#baz2"); $I->fillField("#baz", "baz"); + $I->comment("Exiting Action Group FunctionalActionGroupForMassMergeAfter (keyone)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt b/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt index 110ce3059..5dda9af14 100644 --- a/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt +++ b/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt @@ -27,11 +27,13 @@ class ActionGroupMergedViaInsertBeforeCest */ public function ActionGroupMergedViaInsertBefore(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroupForMassMergeBefore (keyone)"); $I->fillField("#foo", "foo"); $I->click("#foo2"); $I->click("#bar2"); $I->click("#baz2"); $I->fillField("#bar", "bar"); $I->fillField("#baz", "baz"); + $I->comment("Exiting Action Group FunctionalActionGroupForMassMergeBefore (keyone)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupSkipReadiness.txt b/dev/tests/verification/Resources/ActionGroupSkipReadiness.txt index b787f9116..64294b7f8 100644 --- a/dev/tests/verification/Resources/ActionGroupSkipReadiness.txt +++ b/dev/tests/verification/Resources/ActionGroupSkipReadiness.txt @@ -27,8 +27,10 @@ class ActionGroupSkipReadinessCest */ public function ActionGroupSkipReadiness(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithSkipReadinessActions (skipReadinessActionGroup)"); $I->skipReadinessCheck(true); $I->comment("ActionGroupSkipReadiness"); $I->skipReadinessCheck(false); + $I->comment("Exiting Action Group actionGroupWithSkipReadinessActions (skipReadinessActionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupToExtend.txt b/dev/tests/verification/Resources/ActionGroupToExtend.txt index 7c7e666cd..40fdedeb9 100644 --- a/dev/tests/verification/Resources/ActionGroupToExtend.txt +++ b/dev/tests/verification/Resources/ActionGroupToExtend.txt @@ -27,7 +27,9 @@ class ActionGroupToExtendCest */ public function ActionGroupToExtend(AcceptanceTester $I) { + $I->comment("Entering Action Group ActionGroupToExtend (actionGroup)"); $grabProductsActionGroup = $I->grabMultiple("selector"); $I->assertCount(99, $grabProductsActionGroup); + $I->comment("Exiting Action Group ActionGroupToExtend (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt b/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt index f0d14dbd9..f011d307a 100644 --- a/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt +++ b/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt @@ -24,6 +24,7 @@ class ActionGroupUsingCreateDataCest */ public function _before(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithCreateData (Key1)"); $I->amGoingTo("create entity that has the stepKey: createCategoryKey1"); PersistedObjectHandler::getInstance()->createEntity( "createCategoryKey1", @@ -40,6 +41,7 @@ class ActionGroupUsingCreateDataCest ["createCategoryKey1"], null ); + $I->comment("Exiting Action Group actionGroupWithCreateData (Key1)"); } /** diff --git a/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt b/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt index 2d435b051..8d6bf7a3a 100644 --- a/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt @@ -27,7 +27,9 @@ class ActionGroupUsingNestedArgumentCest */ public function ActionGroupUsingNestedArgument(AcceptanceTester $I) { + $I->comment("Entering Action Group ActionGroupToExtend (actionGroup)"); $grabProductsActionGroup = $I->grabMultiple("selector"); $I->assertCount(99, $grabProductsActionGroup); + $I->comment("Exiting Action Group ActionGroupToExtend (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt b/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt index 3bc708a1a..a584c0f50 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt @@ -33,8 +33,10 @@ class ActionGroupWithDataOverrideTestCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -43,8 +45,10 @@ class ActionGroupWithDataOverrideTestCest */ public function _after(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroup (afterGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (afterGroup)"); } /** @@ -68,11 +72,13 @@ class ActionGroupWithDataOverrideTestCest public function ActionGroupWithDataOverrideTest(AcceptanceTester $I) { $I->amOnPage("/someUrl"); + $I->comment("Entering Action Group FunctionalActionGroupWithData (actionGroupWithDataOverride1)"); $I->amOnPage("/John/Doe.html"); $I->fillField("#foo", "John"); $I->fillField("#bar", "Doe"); $I->searchAndMultiSelectOption("#foo", ["John", "Doe"]); $I->see("#element .John"); + $I->comment("Exiting Action Group FunctionalActionGroupWithData (actionGroupWithDataOverride1)"); $I->click("loginButton"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithDataTest.txt b/dev/tests/verification/Resources/ActionGroupWithDataTest.txt index e79534bad..29d822831 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDataTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDataTest.txt @@ -33,8 +33,10 @@ class ActionGroupWithDataTestCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -43,8 +45,10 @@ class ActionGroupWithDataTestCest */ public function _after(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroup (afterGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (afterGroup)"); } /** @@ -68,11 +72,13 @@ class ActionGroupWithDataTestCest public function ActionGroupWithDataTest(AcceptanceTester $I) { $I->amOnPage("/someUrl"); + $I->comment("Entering Action Group FunctionalActionGroupWithData (actionGroupWithData1)"); $I->amOnPage("/Jane/Dane.html"); $I->fillField("#foo", "Jane"); $I->fillField("#bar", "Dane"); $I->searchAndMultiSelectOption("#foo", ["Jane", "Dane"]); $I->see("#element .Jane"); + $I->comment("Exiting Action Group FunctionalActionGroupWithData (actionGroupWithData1)"); $I->click("loginButton"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt index db3c6325c..a66759a89 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt @@ -29,6 +29,8 @@ class ActionGroupWithDefaultArgumentAndStringSelectorParamCest */ public function ActionGroupWithDefaultArgumentAndStringSelectorParam(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithDefaultArgumentAndStringSelectorParam (actionGroup)"); $I->see("John", "#element .test1"); + $I->comment("Exiting Action Group actionGroupWithDefaultArgumentAndStringSelectorParam (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt index e446cc407..e726a178f 100644 --- a/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt @@ -29,6 +29,8 @@ class ActionGroupWithMultipleParameterSelectorsFromDefaultArgumentCest */ public function ActionGroupWithMultipleParameterSelectorsFromDefaultArgument(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithMultipleParameterSelectorsFromArgument (actionGroup)"); $I->see("Doe", "#John-Doe .test"); + $I->comment("Exiting Action Group actionGroupWithMultipleParameterSelectorsFromArgument (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt b/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt index 39e4bda89..751206193 100644 --- a/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt +++ b/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt @@ -29,6 +29,8 @@ class ActionGroupWithNoArgumentsCest */ public function ActionGroupWithNoArguments(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithoutArguments (actionGroup)"); $I->wait(1); + $I->comment("Exiting Action Group actionGroupWithoutArguments (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt b/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt index ed02790e5..fd5846c4a 100644 --- a/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt @@ -33,8 +33,10 @@ class ActionGroupWithNoDefaultTestCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -43,8 +45,10 @@ class ActionGroupWithNoDefaultTestCest */ public function _after(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroup (afterGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (afterGroup)"); } /** @@ -68,9 +72,11 @@ class ActionGroupWithNoDefaultTestCest public function ActionGroupWithNoDefaultTest(AcceptanceTester $I) { $I->amOnPage("/someUrl"); + $I->comment("Entering Action Group FunctionalActionGroupNoDefault (actionGroupWithDataOverride1)"); $I->fillField("#foo", "Jane"); $I->fillField("#bar", "Dane"); $I->see("#Jane .Dane"); + $I->comment("Exiting Action Group FunctionalActionGroupNoDefault (actionGroupWithDataOverride1)"); $I->click("loginButton"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithParameterizedElementWithHyphen.txt b/dev/tests/verification/Resources/ActionGroupWithParameterizedElementWithHyphen.txt index 64a350505..989c5af5e 100644 --- a/dev/tests/verification/Resources/ActionGroupWithParameterizedElementWithHyphen.txt +++ b/dev/tests/verification/Resources/ActionGroupWithParameterizedElementWithHyphen.txt @@ -27,6 +27,8 @@ class ActionGroupWithParameterizedElementWithHyphenCest */ public function ActionGroupWithParameterizedElementWithHyphen(AcceptanceTester $I) { + $I->comment("Entering Action Group SectionArgumentWithParameterizedSelector (actionGroup)"); $keyoneActionGroup = $I->executeJS("#element .full-width"); + $I->comment("Exiting Action Group SectionArgumentWithParameterizedSelector (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt index 157690bee..7df77e742 100644 --- a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt +++ b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt @@ -29,6 +29,8 @@ class ActionGroupWithPassedArgumentAndStringSelectorParamCest */ public function ActionGroupWithPassedArgumentAndStringSelectorParam(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithDefaultArgumentAndStringSelectorParam (actionGroup)"); $I->see("John" . msq("UniquePerson"), "#element .test1"); + $I->comment("Exiting Action Group actionGroupWithDefaultArgumentAndStringSelectorParam (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt b/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt index 5ed6eb7f7..2f58e390c 100644 --- a/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt +++ b/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt @@ -33,8 +33,10 @@ class ActionGroupWithPersistedDataCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -43,8 +45,10 @@ class ActionGroupWithPersistedDataCest */ public function _after(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroup (afterGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (afterGroup)"); } /** @@ -75,10 +79,12 @@ class ActionGroupWithPersistedDataCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroupWithData (actionGroupWithPersistedData1)"); $I->amOnPage("/" . PersistedObjectHandler::getInstance()->retrieveEntityField('createPerson', 'firstname', 'test') . "/" . PersistedObjectHandler::getInstance()->retrieveEntityField('createPerson', 'lastname', 'test') . ".html"); $I->fillField("#foo", PersistedObjectHandler::getInstance()->retrieveEntityField('createPerson', 'firstname', 'test')); $I->fillField("#bar", PersistedObjectHandler::getInstance()->retrieveEntityField('createPerson', 'lastname', 'test')); $I->searchAndMultiSelectOption("#foo", [PersistedObjectHandler::getInstance()->retrieveEntityField('createPerson', 'firstname', 'test'), PersistedObjectHandler::getInstance()->retrieveEntityField('createPerson', 'lastname', 'test')]); $I->see("#element ." . PersistedObjectHandler::getInstance()->retrieveEntityField('createPerson', 'firstname', 'test')); + $I->comment("Exiting Action Group FunctionalActionGroupWithData (actionGroupWithPersistedData1)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt b/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt index 0ee49a80e..34c2a0ea1 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt @@ -27,6 +27,8 @@ class ActionGroupWithSectionAndDataAsArgumentsCest */ public function ActionGroupWithSectionAndDataAsArguments(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithSectionAndData (actionGroup)"); $I->waitForElementVisible("#element .John", 10); + $I->comment("Exiting Action Group actionGroupWithSectionAndData (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt index a71cd562f..f4840d88a 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt @@ -29,6 +29,8 @@ class ActionGroupWithSimpleDataUsageFromDefaultArgumentCest */ public function ActionGroupWithSimpleDataUsageFromDefaultArgument(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithStringUsage (actionGroup)"); $I->see("stringLiteral", "#element .stringLiteral"); + $I->comment("Exiting Action Group actionGroupWithStringUsage (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt index 1453d716f..4f67da1aa 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt @@ -29,15 +29,35 @@ class ActionGroupWithSimpleDataUsageFromPassedArgumentCest */ public function ActionGroupWithSimpleDataUsageFromPassedArgument(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithStringUsage (actionGroup1)"); $I->see("overrideString", "#element .overrideString"); + $I->comment("Exiting Action Group actionGroupWithStringUsage (actionGroup1)"); + $I->comment("Entering Action Group actionGroupWithStringUsage (actionGroup11)"); $I->see("1", "#element .1"); + $I->comment("Exiting Action Group actionGroupWithStringUsage (actionGroup11)"); + $I->comment("Entering Action Group actionGroupWithStringUsage (actionGroup12)"); $I->see("1.5", "#element .1.5"); + $I->comment("Exiting Action Group actionGroupWithStringUsage (actionGroup12)"); + $I->comment("Entering Action Group actionGroupWithStringUsage (actionGroup13)"); $I->see("true", "#element .true"); + $I->comment("Exiting Action Group actionGroupWithStringUsage (actionGroup13)"); + $I->comment("Entering Action Group actionGroupWithStringUsage (actionGroup2)"); $I->see("simpleData.firstname", "#element .simpleData.firstname"); + $I->comment("Exiting Action Group actionGroupWithStringUsage (actionGroup2)"); + $I->comment("Entering Action Group actionGroupWithStringUsage (actionGroup3)"); $I->see(PersistedObjectHandler::getInstance()->retrieveEntityField('persisted', 'data', 'test'), "#element ." . PersistedObjectHandler::getInstance()->retrieveEntityField('persisted', 'data', 'test')); + $I->comment("Exiting Action Group actionGroupWithStringUsage (actionGroup3)"); + $I->comment("Entering Action Group actionGroupWithEntityUsage (actionGroup4)"); $I->see("John", "#element .John"); + $I->comment("Exiting Action Group actionGroupWithEntityUsage (actionGroup4)"); + $I->comment("Entering Action Group actionGroupWithEntityUsage (actionGroup5)"); $I->see(PersistedObjectHandler::getInstance()->retrieveEntityField('simpleData', 'firstname', 'test'), "#element ." . PersistedObjectHandler::getInstance()->retrieveEntityField('simpleData', 'firstname', 'test')); + $I->comment("Exiting Action Group actionGroupWithEntityUsage (actionGroup5)"); + $I->comment("Entering Action Group actionGroupWithEntityUsage (actionGroup6)"); $I->see(PersistedObjectHandler::getInstance()->retrieveEntityField('simpleData', 'firstname[0]', 'test'), "#element ." . PersistedObjectHandler::getInstance()->retrieveEntityField('simpleData', 'firstname[0]', 'test')); + $I->comment("Exiting Action Group actionGroupWithEntityUsage (actionGroup6)"); + $I->comment("Entering Action Group actionGroupWithEntityUsage (actionGroup7)"); $I->see(PersistedObjectHandler::getInstance()->retrieveEntityField('simpleData', 'firstname[data_index]', 'test'), "#element ." . PersistedObjectHandler::getInstance()->retrieveEntityField('simpleData', 'firstname[data_index]', 'test')); + $I->comment("Exiting Action Group actionGroupWithEntityUsage (actionGroup7)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt index 86be7fb72..89979bf97 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt @@ -29,6 +29,8 @@ class ActionGroupWithSingleParameterSelectorFromDefaultArgumentCest */ public function ActionGroupWithSingleParameterSelectorFromDefaultArgument(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithSingleParameterSelectorFromArgument (actionGroup)"); $I->see("Doe", "#element .John"); + $I->comment("Exiting Action Group actionGroupWithSingleParameterSelectorFromArgument (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt index 9a30bcc5c..b4707ff0c 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt @@ -29,6 +29,8 @@ class ActionGroupWithSingleParameterSelectorFromPassedArgumentCest */ public function ActionGroupWithSingleParameterSelectorFromPassedArgument(AcceptanceTester $I) { + $I->comment("Entering Action Group actionGroupWithSingleParameterSelectorFromArgument (actionGroup)"); $I->see("Doe", "#element .John" . msq("UniquePerson")); + $I->comment("Exiting Action Group actionGroupWithSingleParameterSelectorFromArgument (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt b/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt index 542e20133..92b54622c 100644 --- a/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt +++ b/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt @@ -27,6 +27,7 @@ class ActionGroupWithStepKeyReferencesCest */ public function ActionGroupWithStepKeyReferences(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionActionGroupWithStepKeyReferences (actionGroup)"); $I->amGoingTo("create entity that has the stepKey: createSimpleDataActionGroup"); PersistedObjectHandler::getInstance()->createEntity( "createSimpleDataActionGroup", @@ -83,5 +84,6 @@ class ActionGroupWithStepKeyReferencesCest $action14ActionGroup = $I->grabMultiple($action14ActionGroup); $action15ActionGroup = $I->grabTextFrom($action15ActionGroup); $action16ActionGroup = $I->grabValueFrom($action16ActionGroup); + $I->comment("Exiting Action Group FunctionActionGroupWithStepKeyReferences (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt b/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt index acc274567..463e5b307 100644 --- a/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt +++ b/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt @@ -33,8 +33,10 @@ class ActionGroupWithTopLevelPersistedDataCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -43,8 +45,10 @@ class ActionGroupWithTopLevelPersistedDataCest */ public function _after(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroup (afterGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (afterGroup)"); } /** @@ -67,10 +71,12 @@ class ActionGroupWithTopLevelPersistedDataCest */ public function ActionGroupWithTopLevelPersistedData(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroupWithData (actionGroupWithPersistedData1)"); $I->amOnPage("/" . PersistedObjectHandler::getInstance()->retrieveEntityField('createPersonParam', 'firstname', 'test') . "/" . PersistedObjectHandler::getInstance()->retrieveEntityField('createPersonParam', 'lastname', 'test') . ".html"); $I->fillField("#foo", PersistedObjectHandler::getInstance()->retrieveEntityField('createPersonParam', 'firstname', 'test')); $I->fillField("#bar", PersistedObjectHandler::getInstance()->retrieveEntityField('createPersonParam', 'lastname', 'test')); $I->searchAndMultiSelectOption("#foo", [PersistedObjectHandler::getInstance()->retrieveEntityField('createPersonParam', 'firstname', 'test'), PersistedObjectHandler::getInstance()->retrieveEntityField('createPersonParam', 'lastname', 'test')]); $I->see("#element ." . PersistedObjectHandler::getInstance()->retrieveEntityField('createPersonParam', 'firstname', 'test')); + $I->comment("Exiting Action Group FunctionalActionGroupWithData (actionGroupWithPersistedData1)"); } } diff --git a/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt b/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt index 80a1d3c80..d1621fd73 100644 --- a/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt +++ b/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt @@ -33,8 +33,10 @@ class ArgumentWithSameNameAsElementCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -43,8 +45,10 @@ class ArgumentWithSameNameAsElementCest */ public function _after(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroup (afterGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (afterGroup)"); } /** @@ -67,7 +71,9 @@ class ArgumentWithSameNameAsElementCest */ public function ArgumentWithSameNameAsElement(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroupWithTrickyArgument (actionGroup1)"); $I->seeElement("#element"); $I->seeElement("#element .John"); + $I->comment("Exiting Action Group FunctionalActionGroupWithTrickyArgument (actionGroup1)"); } } diff --git a/dev/tests/verification/Resources/BasicActionGroupTest.txt b/dev/tests/verification/Resources/BasicActionGroupTest.txt index d6d953a88..d62c2919d 100644 --- a/dev/tests/verification/Resources/BasicActionGroupTest.txt +++ b/dev/tests/verification/Resources/BasicActionGroupTest.txt @@ -33,8 +33,10 @@ class BasicActionGroupTestCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -49,8 +51,10 @@ class BasicActionGroupTestCest public function BasicActionGroupTest(AcceptanceTester $I) { $I->amOnPage("/someUrl"); + $I->comment("Entering Action Group FunctionalActionGroup (actionGroup1)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (actionGroup1)"); $I->click("loginButton"); } } diff --git a/dev/tests/verification/Resources/BasicMergeTest.txt b/dev/tests/verification/Resources/BasicMergeTest.txt index 039e31a58..2f66f2ce4 100644 --- a/dev/tests/verification/Resources/BasicMergeTest.txt +++ b/dev/tests/verification/Resources/BasicMergeTest.txt @@ -67,11 +67,13 @@ class BasicMergeTestCest $I->fillField("#password", "step5"); $I->click("#step6Merged"); $I->click("#element .Jane .step7Merge"); + $I->comment("Entering Action Group FunctionalActionGroupWithData (step8Merge)"); $I->amOnPage("/Jane/Dane.html"); $I->fillField("#foo", "Jane"); $I->fillField("#bar", "Dane"); $I->searchAndMultiSelectOption("#foo", ["Jane", "Dane"]); $I->see("#element .Jane"); + $I->comment("Exiting Action Group FunctionalActionGroupWithData (step8Merge)"); $I->click("#step10MergedInResult"); } } diff --git a/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt b/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt index cea7ffeca..34106b5fa 100644 --- a/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt +++ b/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt @@ -32,6 +32,6 @@ class ChildExtendedTestNoParentCest */ public function ChildExtendedTestNoParent(AcceptanceTester $I, \Codeception\Scenario $scenario) { - $scenario->skip("This test is skipped due to the following issues:\nNo issues have been specified."); + $scenario->skip("This test is skipped due to the following issues:No issues have been specified."); } } diff --git a/dev/tests/verification/Resources/ExecuteInSeleniumTest.txt b/dev/tests/verification/Resources/ExecuteInSeleniumTest.txt new file mode 100644 index 000000000..323b2ffb9 --- /dev/null +++ b/dev/tests/verification/Resources/ExecuteInSeleniumTest.txt @@ -0,0 +1,32 @@ +executeInSelenium(function ($webdriver) { return 'Hello, World!'}); + } +} diff --git a/dev/tests/verification/Resources/ExtendedActionGroup.txt b/dev/tests/verification/Resources/ExtendedActionGroup.txt index 48fa542bc..e321bfec1 100644 --- a/dev/tests/verification/Resources/ExtendedActionGroup.txt +++ b/dev/tests/verification/Resources/ExtendedActionGroup.txt @@ -27,10 +27,12 @@ class ExtendedActionGroupCest */ public function ExtendedActionGroup(AcceptanceTester $I) { + $I->comment("Entering Action Group extendTestActionGroup (actionGroup)"); $I->comment("New Input Before"); $grabProductsActionGroup = $I->grabMultiple("notASelector"); $I->comment("New Input After"); $I->assertCount(99, $grabProductsActionGroup); $I->assertCount(8000, $grabProductsActionGroup); + $I->comment("Exiting Action Group extendTestActionGroup (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt b/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt index 936e78721..6d7e26d87 100644 --- a/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt +++ b/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt @@ -27,5 +27,7 @@ class ExtendedRemoveActionGroupCest */ public function ExtendedRemoveActionGroup(AcceptanceTester $I) { + $I->comment("Entering Action Group extendRemoveTestActionGroup (actionGroup)"); + $I->comment("Exiting Action Group extendRemoveTestActionGroup (actionGroup)"); } } diff --git a/dev/tests/verification/Resources/MergedActionGroupTest.txt b/dev/tests/verification/Resources/MergedActionGroupTest.txt index 2d1748f56..3a5da8725 100644 --- a/dev/tests/verification/Resources/MergedActionGroupTest.txt +++ b/dev/tests/verification/Resources/MergedActionGroupTest.txt @@ -33,8 +33,10 @@ class MergedActionGroupTestCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -43,8 +45,10 @@ class MergedActionGroupTestCest */ public function _after(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroup (afterGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (afterGroup)"); } /** @@ -67,9 +71,11 @@ class MergedActionGroupTestCest */ public function MergedActionGroupTest(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroupForMerge (actionGroupForMerge)"); $I->see(".merge .Jane"); $I->see("#element .Jane"); $I->amOnPage("/Jane/Dane.html"); $I->click(".merge .Dane"); + $I->comment("Exiting Action Group FunctionalActionGroupForMerge (actionGroupForMerge)"); } } diff --git a/dev/tests/verification/Resources/MultipleActionGroupsTest.txt b/dev/tests/verification/Resources/MultipleActionGroupsTest.txt index 5084692be..ba99de0cb 100644 --- a/dev/tests/verification/Resources/MultipleActionGroupsTest.txt +++ b/dev/tests/verification/Resources/MultipleActionGroupsTest.txt @@ -33,8 +33,10 @@ class MultipleActionGroupsTestCest [], null ); + $I->comment("Entering Action Group FunctionalActionGroup (beforeGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (beforeGroup)"); } /** @@ -43,8 +45,10 @@ class MultipleActionGroupsTestCest */ public function _after(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroup (afterGroup)"); $I->fillField("#foo", "myData1"); $I->fillField("#bar", "myData2"); + $I->comment("Exiting Action Group FunctionalActionGroup (afterGroup)"); } /** @@ -68,16 +72,20 @@ class MultipleActionGroupsTestCest public function MultipleActionGroupsTest(AcceptanceTester $I) { $I->amOnPage("/someUrl"); + $I->comment("Entering Action Group FunctionalActionGroupWithData (actionGroup1)"); $I->amOnPage("/Jane/Dane.html"); $I->fillField("#foo", "Jane"); $I->fillField("#bar", "Dane"); $I->searchAndMultiSelectOption("#foo", ["Jane", "Dane"]); $I->see("#element .Jane"); + $I->comment("Exiting Action Group FunctionalActionGroupWithData (actionGroup1)"); $I->click("loginButton"); + $I->comment("Entering Action Group FunctionalActionGroupWithData (actionGroupWithDataOverride2)"); $I->amOnPage("/John/Doe.html"); $I->fillField("#foo", "John"); $I->fillField("#bar", "Doe"); $I->searchAndMultiSelectOption("#foo", ["John", "Doe"]); $I->see("#element .John"); + $I->comment("Exiting Action Group FunctionalActionGroupWithData (actionGroupWithDataOverride2)"); } } diff --git a/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt b/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt index 02d371571..510e99018 100644 --- a/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt +++ b/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt @@ -27,6 +27,8 @@ class PersistedAndXmlEntityArgumentsCest */ public function PersistedAndXmlEntityArguments(AcceptanceTester $I) { + $I->comment("Entering Action Group FunctionalActionGroupWithXmlAndPersistedData (afterGroup)"); $I->seeInCurrentUrl("/" . PersistedObjectHandler::getInstance()->retrieveEntityField('persistedInTest', 'urlKey', 'test') . ".html?___store=" . msq("uniqueData") . "John"); + $I->comment("Exiting Action Group FunctionalActionGroupWithXmlAndPersistedData (afterGroup)"); } } diff --git a/dev/tests/verification/Resources/PersistenceActionGroupAppendingTest.txt b/dev/tests/verification/Resources/PersistenceActionGroupAppendingTest.txt index c38a14155..e255a0338 100644 --- a/dev/tests/verification/Resources/PersistenceActionGroupAppendingTest.txt +++ b/dev/tests/verification/Resources/PersistenceActionGroupAppendingTest.txt @@ -24,6 +24,7 @@ class PersistenceActionGroupAppendingTestCest */ public function _before(AcceptanceTester $I) { + $I->comment("Entering Action Group DataPersistenceAppendingActionGroup (ACTIONGROUPBEFORE)"); $I->amGoingTo("create entity that has the stepKey: createDataACTIONGROUPBEFORE"); PersistedObjectHandler::getInstance()->createEntity( "createDataACTIONGROUPBEFORE", @@ -53,6 +54,7 @@ class PersistenceActionGroupAppendingTestCest null ); $I->comment(PersistedObjectHandler::getInstance()->retrieveEntityField('createData', 'field', 'hook')); + $I->comment("Exiting Action Group DataPersistenceAppendingActionGroup (ACTIONGROUPBEFORE)"); } /** @@ -64,6 +66,7 @@ class PersistenceActionGroupAppendingTestCest */ public function PersistenceActionGroupAppendingTest(AcceptanceTester $I) { + $I->comment("Entering Action Group DataPersistenceAppendingActionGroup (ACTIONGROUP)"); $I->amGoingTo("create entity that has the stepKey: createDataACTIONGROUP"); PersistedObjectHandler::getInstance()->createEntity( "createDataACTIONGROUP", @@ -93,5 +96,6 @@ class PersistenceActionGroupAppendingTestCest null ); $I->comment(PersistedObjectHandler::getInstance()->retrieveEntityField('createDataACTIONGROUP', 'field', 'test')); + $I->comment("Exiting Action Group DataPersistenceAppendingActionGroup (ACTIONGROUP)"); } } diff --git a/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt b/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt index 24faac699..2db1b498f 100644 --- a/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt +++ b/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt @@ -74,6 +74,7 @@ class PersistenceCustomFieldsTestCest ["createdData"], $createdData3Fields ); + $I->comment("Entering Action Group PersistenceActionGroup (createdAG)"); $createDataAG1CreatedAGFields['firstname'] = "string1"; $I->amGoingTo("create entity that has the stepKey: createDataAG1CreatedAG"); PersistedObjectHandler::getInstance()->createEntity( @@ -101,5 +102,6 @@ class PersistenceCustomFieldsTestCest [], $createDataAG3CreatedAGFields ); + $I->comment("Exiting Action Group PersistenceActionGroup (createdAG)"); } } diff --git a/dev/tests/verification/Resources/functionalSuiteHooks.txt b/dev/tests/verification/Resources/functionalSuiteHooks.txt index 0a6dc463f..d9ff2a344 100644 --- a/dev/tests/verification/Resources/functionalSuiteHooks.txt +++ b/dev/tests/verification/Resources/functionalSuiteHooks.txt @@ -60,7 +60,9 @@ class functionalSuiteHooks extends \Codeception\GroupObject $createFields ); $webDriver->click(PersistedObjectHandler::getInstance()->retrieveEntityField('create', 'data', 'suite')); + print("Entering Action Group actionGroupWithTwoArguments (AC)"); $webDriver->see("John", msq("uniqueData") . "John"); + print("Exiting Action Group actionGroupWithTwoArguments (AC)"); // reset configuration and close session $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver')->_resetConfig(); @@ -120,7 +122,9 @@ class functionalSuiteHooks extends \Codeception\GroupObject $webDriver->_initializeSession(); $webDriver->amOnPage("some.url"); $webDriver->deleteEntityByUrl("deleteThis"); + print("Entering Action Group actionGroupWithTwoArguments (AC)"); $webDriver->see("John", msq("uniqueData") . "John"); + print("Exiting Action Group actionGroupWithTwoArguments (AC)"); } catch (\Exception $exception) { print $exception->getMessage(); } diff --git a/dev/tests/verification/TestModule/Test/ExecuteInSeleniumTest.xml b/dev/tests/verification/TestModule/Test/ExecuteInSeleniumTest.xml new file mode 100644 index 000000000..0a72189b4 --- /dev/null +++ b/dev/tests/verification/TestModule/Test/ExecuteInSeleniumTest.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/dev/tests/verification/Tests/ExecuteInSeleniumTest.php b/dev/tests/verification/Tests/ExecuteInSeleniumTest.php new file mode 100644 index 000000000..36532b362 --- /dev/null +++ b/dev/tests/verification/Tests/ExecuteInSeleniumTest.php @@ -0,0 +1,22 @@ +generateAndCompareTest('ExecuteInSeleniumTest'); + } +} diff --git a/docs/best-practices.md b/docs/best-practices.md new file mode 100644 index 000000000..180f815ca --- /dev/null +++ b/docs/best-practices.md @@ -0,0 +1,166 @@ +# Best practices + +Check out our best practices below to ensure you are getting the absolute most out of the Magento Functional Testing Framework. + +## Action group + +1. [Action group] names should be sufficiently descriptive to inform a test writer of what the action group does and when it should be used. + Add additional explanation in comments if needed. +2. Provide default values for the arguments that apply to your most common case scenarios. + +## `actionGroups` vs `extends` + +Use an action group to wraps a set of actions to reuse them multiple times. + +Use an [extension] when a test or action group needs to be repeated with the exception of a few steps. + +### When to use `extends` + +Use `extends` in your new test or action group when at least one of the following conditions is applicable to your case: + +1. You want to keep the original test without any modifications. +2. You want to create a new test that follows the same path as the original test. +3. You want a new action group that behaves similarly to the existing action group, but you do not want to change the functionality of the original action group. + +### When to avoid `extends` + +Do not use `extends` in the following conditions: + +1. You want to change the functionality of the test or action group and do not need to run the original version. +2. You plan to merge the base test or action group. + +The following pattern is used when merging with `extends`: + +1. The original test is merged. +2. The extended test is created from the merged original test. +3. The extended test is merged. + +## Annotation + +1. Use [annotations] in a test. +2. Update your annotations correspondingly when updating tests. + +## Data entity + +1. Keep your testing instance clean. + Remove data after the test if the test required creating any data. + Use a corresponding [``] test step in your [``] block when using a [``] action in a [``] block. +2. Make specific data entries under test to be unique. + Enable data uniqueness where data values are required to be unique in a database by test design. + Use `unique=”suffix”` or `unique=”prefix”` to append or prepend a unique value to the [entity] attribute. + This ensures that tests using the entity can be repeated. +3. Do not modify existing data entity fields or merge additional data fields without complete understanding and verifying the usage of existing data in tests. + Create a new data entity for your test if you are not sure. + +## Naming conventions + +### File names + +Name files according to the following patterns to make searching in future more easy: + +#### Test file name + +Format: {_Admin_ or _Storefront_}{Functionality}_Test.xml_, where Functionality briefly describes the testing functionality. + +Example: _StorefrontCreateCustomerTest.xml_. + +#### Section file name + +Format: {_Admin_ or _Storefront_}{UI Description}_Section.xml_, where UI Description briefly describes the testing UI. + +Example: _AdminNavbarSection.xml_. + +#### Data file name + +Format: {Type}_Data.xml_, where Type represents the entity type. + +Example: _ProductData.xml_. + +### Object names + +Use the _Foo.camelCase_ naming convention, which is similar to _Classes_ and _classProperties_ in PHP. + +#### Upper case + +Use an upper case first letter for: + +- File names. Example: _StorefrontCreateCustomerTest.xml_ +- Test name attributes. Example: ``. +- Data entity names. Example: ``. +- Page name. Example: ``. +- Section name. Example: `
`. +- Action group name. Example: ``. + +#### Lower case + +Use a lower case first letter for: + +- Data keys. Example: ``. +- Element names. Examples: ``. + +## Page object + +Use [parameterized selectors] for constructing a selector when test specific or runtime generated information is needed. +Do not use them for static elements. + + +BAD: + + + + +``` xml + +``` + + + + +GOOD: + + +Define these three elements and reference them by name in the tests. + +``` xml + + + +``` + +## Test + +1. Use actions such as [``], [``], and [``] to wait the exact time required for the test step. + Try to avoid using the [``] action, because it forces the test to wait for the time you specify. You may not need to wait so long to proceed. +1. Keep your tests short and granular for target testing, easier reviews, and easier merge conflict resolution. + It also helps you to identify the cause of test failure. +1. Use comments to keep tests readable and maintainable: + - Keep the inline `` and [``] tags up to date. + It helps to inform the reader of what you are testing and to yield a more descriptive Allure report. + - Explain in comments unclear or tricky test steps. +1. Refer to [sections] instead of writing selectors. + +## Test step merging order + +When setting a [merging] order for a test step, do not depend on steps from Magento modules that could be disabled by an application. + +For example, when you write a test step to create a gift card product, set your test step **after** simple product creation and let the MFTF handle the merge order. +Since the configurable product module could be disabled, this approach is more reliable than setting the test step **before** creating a configurable product. + + + +[``]: test/actions.html#before-and-after +[``]: test/actions.html#before-and-after +[``]: test/actions.html#comment +[``]: test/actions.html#createdata +[``]: test/actions.html#deletedata +[``]: test/actions.html#wait +[``]: test/actions.html#waitforelement +[``]: test/actions.html#waitforelementvisible +[``]: test/actions.html#waitforloadingmasktodisappear +[Action group]: test/action-groups.html +[annotations]: test/annotations.html +[entity]: data.html +[extension]: extending.html +[merging]: merging.html +[parameterized selectors]: section/parameterized-selectors.html +[sections]: section.html diff --git a/docs/commands/codeception.md b/docs/commands/codeception.md new file mode 100644 index 000000000..6a96a89c7 --- /dev/null +++ b/docs/commands/codeception.md @@ -0,0 +1,85 @@ +# CLI commands: vendor/bin/codecept + +
+We do not recommend using Codeception commands directly as they can break the MFTF basic workflow. +All the Codeception commands you need are wrapped using the [mftf tool][]. + +To run the Codeception testing framework commands directly, change your directory to the ``. +
+ +## Usage examples + +Run all the generated tests: + +```bash +vendor/bin/codecept run functional -c dev/tests/acceptance/codeception.yml +``` + +Run all tests without the `` [annotation][]: + +```bash +vendor/bin/codecept run functional --skip-group skip -c dev/tests/acceptance/codeception.yml +``` + +Run all tests with the `` [annotation][] but with no ``: + +```bash +vendor/bin/codecept run functional --group example --skip-group skip -c dev/tests/acceptance/codeception.yml +``` + +## `codecept run` + +`codecept run` runs the test suites: + +```bash +vendor/bin/codecept run +``` + +
+The following documentation corresponds to Codeception 2.3.8. +
+ +```bash +Full reference: + +Arguments: + suite suite to be tested + test test to be run + +Options: + -o, --override=OVERRIDE Override config values (multiple values allowed) + --config (-c) Use custom path for config + --report Show output in compact style + --html Generate html with results (default: "report.html") + --xml Generate JUnit XML Log (default: "report.xml") + --tap Generate Tap Log (default: "report.tap.log") + --json Generate Json Log (default: "report.json") + --colors Use colors in output + --no-colors Force no colors in output (useful to override config file) + --silent Only outputs suite names and final results + --steps Show steps in output + --debug (-d) Show debug and scenario output + --coverage Run with code coverage (default: "coverage.serialized") + --coverage-html Generate CodeCoverage HTML report in path (default: "coverage") + --coverage-xml Generate CodeCoverage XML report in file (default: "coverage.xml") + --coverage-text Generate CodeCoverage text report in file (default: "coverage.txt") + --coverage-phpunit Generate CodeCoverage PHPUnit report in file (default: "coverage-phpunit") + --no-exit Do not finish with exit code + --group (-g) Groups of tests to be executed (multiple values allowed) + --skip (-s) Skip selected suites (multiple values allowed) + --skip-group (-x) Skip selected groups (multiple values allowed) + --env Run tests in selected environments. (multiple values allowed, environments can be merged with ',') + --fail-fast (-f) Stop after first failure + --help (-h) Display this help message. + --quiet (-q) Do not output any message. + --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + --version (-V) Display this application version. + --ansi Force ANSI output. + --no-ansi Disable ANSI output. + --no-interaction (-n) Do not ask any interactive question. +``` + + + +[mftf tool]: mftf.md +[annotation]: ../test/annotations.md \ No newline at end of file diff --git a/docs/commands/mftf.md b/docs/commands/mftf.md new file mode 100644 index 000000000..4d91bb37f --- /dev/null +++ b/docs/commands/mftf.md @@ -0,0 +1,443 @@ +# CLI commands: vendor/bin/mftf + +The Magento Functional Testing Framework (MFTF) introduces the command line interface (CLI) tool `vendor/bin/mftf` to facilitate your interaction with the framework. + +Note that `mftf` commands replace the `robo` commands that were used in previous releases. + +## Command format + +In the project root directory (where you have installed the framework as a composer dependency), run commands using the following format: + +```bash +vendor/bin/mftf command [options] [] [--remove|-r] +``` + +## Useful commands + +Use the following commands to run commonly performed tasks. + +### Apply the configuration parameters + +```bash +vendor/bin/mftf build:project +``` + +### Upgrade the project + +```bash +vendor/bin/mftf build:project --upgrade +``` + +Upgrades the existing MFTF tests after the MFTF major upgrade. + +### Generate all tests + +```bash +vendor/bin/mftf generate:tests +``` + +### Generate tests by test name + +```bash +vendor/bin/mftf generate:tests LoginAsAdminTest LoginAsCustomerTest +``` + +### Generate and run the tests for a specified group + +```bash +vendor/bin/mftf run:group product -r +``` + +This command cleans up the previously generated tests; generates and runs tests for the product group (where `group="product"`). + +### Generate and run particular tests + +```bash +vendor/bin/mftf run:test LoginAsAdminTest LoginAsCustomerTest -r +``` + +This command cleans up the previously generated tests; generates and runs the `LoginAsAdminTest` and `LoginAsCustomerTest` tests. + +### Generate and run previously failed tests + +```bash +vendor/bin/mftf run:failed +``` + +This command cleans up the previously generated tests; generates and runs the tests listed in `dev/tests/acceptance/tests/_output/failed`. +For more details about `failed`, refer to [Reporting][]. + +### Generate documentation for action groups + +```bash +vendor/bin/mftf generate:docs +``` + +This command generates documentation for action groups. + +## Reference + +### `build:project` + +#### Description + +Clone the example configuration files and build the Codeception project. + +#### Usage + +```bash +vendor/bin/mftf build:project [--upgrade] [config_param_options] +``` + +#### Options + +| Option | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `-u`, `--upgrade` | Upgrades existing MFTF tests according to requirements of the last major release. Specifying this flag upgrades only those tests in the default location. Example: `build:project --upgrade`. | + +You can include options to set configuration parameter values for your environment since the project build process also [sets up the environment][setup]. + +```bash +vendor/bin/mftf build:project --MAGENTO_BASE_URL=http://magento.local/ --MAGENTO_BACKEND_NAME=admin214365 +``` + +### `generate:tests` + +#### Description + +Generate PHP code from the tests defined in XML files. +The path is set in the `TESTS_MODULE_PATH` [configuration] parameter. + +#### Usage + +```bash +vendor/bin/mftf generate:tests [option] [] [] [--remove] +``` + +#### Options + +| Option | Description| +| ---| --- | +| `--config=[ or or ]` | Creates a single manifest file with a list of all tests. The default location is `tests/functional/Magento/FunctionalTest/_generated/testManifest.txt`.
You can split the list into multiple groups using `--config=parallel`; the groups will be generated in `_generated/groups/` like `_generated/groups/group1.txt, group2.txt, ...`.
Available values: `default` (default), `singleRun`(same as `default`), and `parallel`.
Example: `generate:tests --config=parallel`. | +| `--force` | Forces test generation, regardless of the module merge order defined in the Magento instance. Example: `generate:tests --force`. | +| `-i,--time` | Set time in minutes to determine the group size when `--config=parallel` is used. The __default value__ is `10`. Example: `generate:tests --config=parallel --time=15`| +| `--tests` | Defines the test configuration as a JSON string.| +| `--debug` | Returns additional debug information (such as the filename where an error occurred) when test generation fails because of an invalid XML schema. This parameter takes extra processing time. Use it after test generation has failed once. | +| `-r,--remove`| Removes the existing generated suites and tests cleaning up the `_generated` directory before the actual run. For example, `generate:tests SampleTest --remove` cleans up the entire `_generated` directory and generates `SampleTest` only.| + +#### Examples of the JSON configuration + +The configuration to generate a single test with no suites: + +```json +{ + "tests":[ + "general_test1" //Generate the "general_test1" test. + ], + "suites": null +} +``` + +The configuration to generate a single test in the suite: + +```json +{ + "tests": null, // No tests outside the suite configuration will be generated. + "suites":{ + "sample":[ // The suite that contains the test. + "suite_test1" // The test to be generated. + ] + } +} +``` + +Complex configuration to generate a few non-suite tests, a single test in a suite, and an entire suite: + +```json +{ + "tests":[ + "general_test1", + "general_test2", + "general_test3" + ], + "suites":{ //Go to suites. + "sample":[ //Go to the "sample" suite. + "suite_test1" //Generate the "suite_test1" test. + ], + "sample2":[] //Generate all tests in the "sample2" suite. + } +} +``` + +The command that encodes this complex configuration: + +```bash +vendor/bin/mftf generate:tests --tests "{\r\n\"tests\":[\r\n\"general_test1\",\r\n\"general_test2\",\r\n\"general_test3\"\r\n],\r\n\"suites\":{\r\n\"sample\":[\r\n\"suite_test1\"\r\n],\r\n\"sample2\":null\r\n}\r\n}" +``` + +Note that the strings must be escaped and surrounded in quotes. + +### `generate:suite` + +#### Description + +Generates one or more suites based on XML declarations. + +#### Usage + +```bash +vendor/bin/mftf generate:suite [] [--remove] +``` + +#### Options + +| Option | Description | +| --- | --- | +| `-r,--remove` | Removes the existing generated suites and tests cleaning up the `_generated` directory before the actual run. For example, `vendor/bin/mftf generate:suite WYSIWYG --remove` cleans up the entire `_generated` directory and generates `WYSIWYG` only. | + +#### Example + +```bash +vendor/bin/mftf generate:suite suite1 suite2 +``` + +### `generate:urn-catalog` + +#### Description + +Generates a URN catalog, enabling PhpStorm to recognize and highlight URNs. +It also enables auto-completion in PhpStorm. + +#### Usage + +```bash +vendor/bin/mftf generate:urn-catalog [--force] [] +``` + +`misc.xml` is typically located in `/.idea/`. + +#### Options + +| Option | Description | +| ------------- | --------------------------------------------------------------------- | +| `-f, --force` | Creates the `misc.xml` file if it does not exist in the given `path`. | + +#### Example + +```bash +vendor/bin/mftf generate:urn-catalog .idea/ +``` + +### `generate:docs` + +#### Description + +Generates documentation that lists all action groups available in the codebase. +The default path is `/dev/tests/docs/documentation.md`. + +#### Usage + +```bash +vendor/bin/mftf generate:docs [--clean] [--output=/path/to/alternate/dir] +``` + +#### Options + +| Option | Description | +| ------------- | --------------------------------------------------------------------- | +| `-c, --clean` | Overwrites previously existing documentation | +| `-o, --output` | Changes the default output directory to a user specified directory | + +#### Example + +```bash +vendor/bin/mftf generate:docs --clean +``` + +### `reset` + +#### Description + +Cleans any configuration files and generated artifacts from the environment. +The `.env` file is not affected. + +#### Usage + +```bash +vendor/bin/mftf reset [--hard] +``` + +#### Options + +| Option | Description | +| -------- | ------------------------------------------ | +| `--hard` | Forces a reset of the configuration files. | + +#### Example + +```bash +vendor/bin/mftf reset --hard +``` + +### `run:group` + +Generates and executes the listed groups of tests using Codeception. + +#### Usage + +```bash +vendor/bin/mftf run:group [--skip-generate|--remove] [--] [] +``` + +#### Options + +| Option | Description | +| --------------------- | --------------------------------------------------------------------------------------------------------- | +| `-k, --skip-generate` | Skips generating from the source XML. Instead, the command executes previously-generated groups of tests. | +| `-r, --remove` | Removes previously generated suites and tests before the actual generation and run. | + +#### Examples + +Clean up after the last test run; generate from XML and execute the tests with the annotations `group="group1"` and `group="group2"`: + +```bash +vendor/bin/mftf -r -- run:group group1 group2 +``` + +Execute previously generated tests with the annotations `group="group1"` and `group="group2"`, skipping the regeneration of the test: + +```bash +vendor/bin/mftf run:group -k -- group1 group2 +``` + +### `run:test` + +Generates and executes tests by name using Codeception. + +#### Usage + +```bash +vendor/bin/mftf run:test [--skip-generate|--remove] [--] [] +``` + +#### Options + +| Option | Description | +|-----------------------|-----------------------------------------------------------------------------------------------------------| +| `-k, --skip-generate` | Skips generating from the source XML. Instead, the command executes previously-generated groups of tests. | +| `-r, --remove` | Remove previously generated suites and tests. | + +#### Examples + +Generate the `LoginCustomerTest` and `StorefrontCreateCustomerTest` tests from XML and execute all the generated tests: + +```bash +vendor/bin/mftf run:test LoginCustomerTest StorefrontCreateCustomerTest +``` + +### `run:failed` + +Regenerates and reruns tests that previously failed. + +This command cleans up previously generated tests. It generates and runs the tests listed in `dev/tests/acceptance/tests/_output/failed`. +For more details about `failed`, refer to [Reporting][]. + +#### Usage + +```bash +vendor/bin/mftf run:failed +``` + +#### Examples + +Run the tests that failed in the previous run: + +```bash +vendor/bin/mftf run:failed +``` + +### `setup:env` + +Updates the [configuration] parameter values in the [`.env`] file. +Creates the `.env` file if it does not exist. + +#### Usage + +```bash +vendor/bin/mftf setup:env [config_param_option1=] [config_param_option2=] +``` + +`config_param` is a configuration parameter from the `.env` file. +The command consumes the parameters in a format of options assigned with values, for example `--MAGENTO_BASE_URL=http://magento.local/`. +If you specify a parameter that the `.env` file does not contain, the command returns an error. + +You can also update configuration parameter values when you use the [`build:project`][build] command. + +#### Examples + +To change values for the `MAGENTO_BASE_URL` and `BROWSER`: + +```bash +vendor/bin/mftf setup:env --MAGENTO_BASE_URL=http://magento.local/ --BROWSER=firefox +``` + +To create a `.env` file with example parameters: + +```bash +vendor/bin/mftf setup:env +``` + +The example parameters are taken from the `etc/config/.env.example` file. + +### `static:checks` + +Runs all MFTF static:checks on the test codebase that MFTF is currently attached to. + +#### Existing static checks + +* Test Dependency: Checks that test dependencies do not violate Magento module's composer dependencies. + +#### Usage + +```bash +vendor/bin/mftf static:checks +``` + +### `upgrade:tests` + +Applies all the MFTF major version upgrade scripts to test components in the given path (`test.xml`, `data.xml`, etc). + +#### Usage + +```bash +vendor/bin/mftf upgrade:tests +``` + +`` is the path that contains MFTF test components that need to be upgraded. +The command searches recursively for any `*.xml` files to upgrade. + +#### Examples + +To upgrade all test components inside modules in the `dev/tests/acceptance/tests/` directory: + +```bash +vendor/bin/mftf upgrade:tests /Users/user/magento2/dev/tests/acceptance/tests/ +``` + +To upgrade all test components inside the `Catalog` module: + +```bash +vendor/bin/mftf upgrade:tests /Users/user/magento2/app/code/Magento/Catalog/Test/Mftf/ +``` + + + +[configuration]: ../configuration.md +[Reference]: #reference +[build]: #buildproject +[setup]: #setupenv +[Reporting]: ../reporting.md + + + +*[MFTF]: Magento Functional Testing Framework diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 000000000..9d87e0735 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,259 @@ +# Configuration + +The `*.env` file provides additional configuration for the Magento Functional Testing Framework (MFTF). +To run the MFTF on your Magento testing instance, specify the basic configuration values. +Advanced users can create custom configurations based on requirements and environment. + +## Basic configuration + +These basic configuration values are __required__ and must be set by the user before the MFTF can function correctly. + +### MAGENTO_BASE_URL + +The root URL of the Magento application under test. + +Example: + +```conf +MAGENTO_BASE_URL=http://magento2.vagrant251 +``` + +
+If the `MAGENTO_BASE_URL` contains a subdirectory (like `http://magento.test/magento2ce`), specify [`MAGENTO_CLI_COMMAND_PATH`][]. +
+ +### MAGENTO_BACKEND_NAME + +The path to the Magento Admin page. + +Example: + +```conf +MAGENTO_BACKEND_NAME=admin_12346 +``` + +### MAGENTO_ADMIN_USERNAME + +The username that tests can use to access the Magento Admin page + +Example: + +```conf +MAGENTO_ADMIN_USERNAME=admin +``` + +### MAGENTO_ADMIN_PASSWORD + +The password that tests will use to log in to the Magento Admin page. + +Example: + +```conf +MAGENTO_ADMIN_PASSWORD=1234reTyt%$7 +``` + +## Advanced configuration + +Depending on the environment you use, you may need to configure the MFTF more precisely by setting more configuration parameters then for basic configuration. +This section describes available configuration parameters and their default values (where applicable). + +### DEFAULT_TIMEZONE + +Sets a default value for the `timezone` attribute of a [`generateDate` action][generateDate]. +This value is applied when a test step does not specify a time zone. +For the complete list of available time zones, refer to [List of Supported Timezones][timezones]. + +Default: `America/Los_Angeles`. + +Example: + +```conf +DEFAULT_TIMEZONE=UTC +``` + +### SELENIUM + +The `SELENIUM_*` values form the URL of a custom Selenium server for running testing. + +Default Selenium URL: `http://127.0.0.1:4444/wd/hub` + +And the default configuration: + +```conf +SELENIUM_HOST=127.0.0.1 +SELENIUM_PORT=4444 +SELENIUM_PROTOCOL=http +SELENIUM_PATH=/wd/hub +``` + +
+`SELENIUM_*` values are required if you are running Selenium on an external system. +If you change the configuration of the external Selenium server, you must update these values. +
+ +#### SELENIUM_HOST + +Override the default Selenium server host. + +Example: + +```conf +SELENIUM_HOST=user:pass@ondemand.saucelabs.com +``` + +#### SELENIUM_PORT + +Override the default Selenium server port. + +Example: + +```conf +SELENIUM_PORT=443 +``` + +#### SELENIUM_PROTOCOL + +Override the default Selenium server protocol. + +Example: + +```conf +SELENIUM_PROTOCOL=https +``` + +#### SELENIUM_PATH + +Override the default Selenium server path. + +Example: + +```conf +SELENIUM_PATH=/wd/hub +``` + +### MAGENTO_RESTAPI + +These `MAGENTO_RESTAPI_*` values are optional and can be used in cases when your Magento instance has a different API path than the one in `MAGENTO_BASE_URL`. + +```conf +MAGENTO_RESTAPI_SERVER_HOST +MAGENTO_RESTAPI_SERVER_PORT +``` + +#### MAGENTO_RESTAPI_SERVER_HOST + +The protocol and the host of the REST API server path. + +Example: + +```conf +MAGENTO_RESTAPI_SERVER_HOST=http://localhost +``` + +#### MAGENTO_RESTAPI_SERVER_PORT + +The port part of the API path. + +Example: + +```conf +MAGENTO_RESTAPI_SERVER_PORT=5000 +``` + +### \*_BP + +Settings to override base paths for the framework. +You can use it when the MFTF is applied as a separate tool. +For example, when you need to place the MFTF and the Magento codebase in separate projects. + +```conf +MAGENTO_BP +TESTS_BP +FW_BP +TESTS_MODULES_PATH +``` + +#### MAGENTO_BP + +The path to a local Magento codebase. +It enables the [`bin/mftf`][mftf] commands such as `run` and `generate` to parse all modules of the Magento codebase for MFTF tests. + +```conf +MAGENTO_BP=~/magento2/ +``` + +#### TESTS_BP + +BP is an acronym for _Base Path_. +The path to where MFTF supplementary files are located in the Magento codebase. + +Example: + +```conf +TESTS_BP=~/magento2ce/dev/tests/acceptance +``` + +#### FW_BP + +The path to MFTF. +FW_BP is an acronym for _FrameWork Base Path_. + +Example: + +```conf +FW_BP=~/magento/magento2-functional-testing-framework +``` + +#### TESTS_MODULE_PATH + +The path to where the MFTF modules mirror Magento modules. + +Example: + +```conf +TESTS_MODULE_PATH=~/magento2/dev/tests/acceptance/tests/functional/Magento/FunctionalTest +``` + +### MODULE_WHITELIST + +Use for a new module. +When adding a new directory at `Magento/FunctionalTest`, add the directory name to `MODULE_WHITELIST` to enable the MFTF to process it. + +Example: + +```conf +MODULE_WHITELIST=Magento_Framework,Magento_ConfigurableProductWishlist,Magento_ConfigurableProductCatalogSearch +``` + +### MAGENTO_CLI_COMMAND_PATH + +Path to the Magento CLI command entry point. + +Default: `dev/tests/acceptance/utils/command.php`. +It points to `MAGENTO_BASE_URL` + `dev/tests/acceptance/utils/command.php` + +Modify the default value: + +- for non-default Magento installation +- when use a subdirectory in the `MAGENTO_BASE_URL` + +Example: `dev/tests/acceptance/utils/command.php` + +### BROWSER + +Override the default browser performing the tests. + +Default: Chrome + +Example: + +```conf +BROWSER=firefox +``` + + + +[`MAGENTO_CLI_COMMAND_PATH`]: #magento_cli_command_path +[generateDate]: test/actions.md#generatedate +[mftf]: commands/mftf.md +[timezones]: http://php.net/manual/en/timezones.php \ No newline at end of file diff --git a/docs/credentials.md b/docs/credentials.md new file mode 100644 index 000000000..7afa9b805 --- /dev/null +++ b/docs/credentials.md @@ -0,0 +1,100 @@ +# Credentials + +When you test functionality that involves external services such as UPS, FedEx, PayPal, or SignifyD, use the MFTF credentials feature to hide sensitive [data][] like integration tokens and API keys. + +## Define sensitive data in `.credentials` + +The MFTF creates a sample file for credentials during [initial setup][]: `magento2/dev/tests/acceptance/.credentials.example`. +The file contains an example list of keys for fields that can require credentials. + +### Create `.credentials` + +To make the MFTF process the file with credentials, change directories to `magento2/dev/tests/acceptance/` and copy `.credentials.example` to `.credentials`. + +```bash +cd dev/tests/acceptance/ +``` + +```bash +cp .credentials.example .credentials +``` + +### Add `.credentials` to `.gitignore` + +Verify that the file is excluded from tracking by `.gitignore` (unless you need this behavior): + +```bash +git check-ignore .credentials +``` + +The command outputs the path if the file is excluded: + +```terminal +.credentials +``` + +### Define sensitive data + +Open the `.credentials` file, uncomment the fields you want to use, and add your values: + +```config +... +# Credentials for the USPS service +carriers_usps_userid=test_user +carriers_usps_password=Lmgxvrq89uPwECeV + +# Credentials for the DHL service +#carriers/dhl/id_us= +#carriers/dhl/password_us= +.... +``` + +
+The `/` symbol is not supported in a key name. +
+ +You are free to use any other keys you like, as they are merely the keys to reference from your tests. + +```conf +# Credentials for the MyAwesome service +my_awesome_service_token=rRVSVnh3cbDsVG39oTMz4A + +# Credentials for the USPS service +carriers_usps_userid=test_user +carriers_usps_password=Lmgxvrq89uPwECeV +.... +``` + + + +## Use credentials in a test + +Access the data defined in the `.credentials` file using the [`fillField`][] action with the `userInput` attribute. +Define the value as a reference to the corresponding key in the credentials file such as `{{_CREDS.my_data_key}}`: + +- `_CREDS` is an environment constant pointing to the `.credentials` file +- `my_data_key` is a key in the the `.credentials` file that contains the value to be used in a test step + +For example: + +```xml + +``` + + + +## Implementation details + +The generated tests do not contain credentials values. +The MFTF dynamically retrieves, encrypts, and decrypts the sensitive data during test execution. +Decrypted credentials do not appear in the console, error logs, or [test reports][]. +The decrypted values are only available in the `.credentials` file. + +
+The MFTF tests delivered with Magento application do not use credentials and do not cover external services, because of sensitivity of the data.
+ + +[`fillField`]: test/actions.md#fillfield +[data]: data.md +[initial setup]: getting-started.md +[test reports]: reporting.md diff --git a/docs/data.md b/docs/data.md new file mode 100644 index 000000000..42736a6de --- /dev/null +++ b/docs/data.md @@ -0,0 +1,266 @@ +# Input testing data + +The MFTF enables you to specify and use `` entities defined in XML. Default `` entities are provided for use and as templates for entity creation and manipulation. +The following diagram shows the XML structure of an MFTF data object: + +![MFTF Data Object](img/data-dia.svg) + + + +## Supply data to test by reference to a data entity + +Test steps requiring `` input in an action, like filling a field with a string, may reference an attribute from a data entity: + +```xml +userInput="{{SimpleSubCategory.name}}" +``` + +In this example: + +* `SimpleSubCategory` is an entity name. +* `name` is a `` key of the entity. The corresponding value will be assigned to `userInput` as a result. + +### Environmental data + +```xml +userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" +``` + +In this example: + +* `_ENV` is a reference to the `dev/tests/acceptance/.env` file, where basic environment variables are set. +* `MAGENTO_ADMIN_USERNAME` is a name of an environment variable. + The corresponding value will be assigned to `userInput` as a result. + +### Sensitive data + +```xml +userInput="{{_CREDS.my_secret_token}}" +``` + +In this example: + +* `_CREDS` is a constant to reference to the `dev/tests/acceptance/.credentials` file, where sensitive data and secrets are stored for use in a test. +* `MY_SECRET_TOKEN` is the name of a key in the credentials variable. + The corresponding value of the credential will be assigned to `userInput` as a result. +* The decrypted values are only available in the `.credentials` file in which they are stored. + +Learn more in [Credentials][]. + +## Persist a data entity as a prerequisite of a test {#persist-data} + +A test can specify an entity to be persisted (created in the database) so that the test actions could operate on the existing known data. + +Example of referencing `data` in a test: + +```xml +userInput="$createCustomer.email$" +``` + +In this example: + +* `createCustomer` is a step key of the corresponding test step that creates an entity. +* `email` is a data key of the entity. + The corresponding value will be assigned to `userInput` as a result. + +
+As of MFTF 2.3.6, you no longer need to differentiate between scopes (a test, a hook, or a suite) for persisted data when referencing it in tests. +
+ +The MFTF now stores the persisted data and attempts to retrieve it using the combination of `stepKey` and the scope of where it has been called. +The current scope is preferred, then widening to _test > hook > suite_ or _hook > test > suite_. + +This emphasizes the practice for the `stepKey` of `createData` to be descriptive and unique, as a duplicated `stepKey` in both a `` and `` prefers the `` data. + +## Use data returned by test actions + +A test can also reference data that was returned as a result of [test actions][], like the action ` + +``` + +## Hard-coded data input + +The data to operate against can be included as literals in a test. Hard-coded data input can be useful in assertions. + +See also [Actions][]. + +```xml +userInput="We'll email you an order confirmation with details and tracking info." +``` + +## Format + +The format of `` is: + +```xml + + + + + + + + + + + +``` + +## Principles + +The following conventions apply to MFTF ``: + +* A `` file may contain multiple data entities. +* Camel case is used for `` elements. The name represents the `` type. For example, a file with customer data is `CustomerData.xml`. A file for simple product would be `SimpleProductData.xml`. +* Camel case is used for the entity name. + +## Example + +Example (`.../Catalog/Data/CategoryData.xml` file): + +```xml + + + + + simpleCategory + simplecategory + true + + + SimpleSubCategory + simplesubcategory + true + true + + +``` + +This example declares two `` entities: `_defaultCategory` and `SimpleSubCategory`. They set the data required for [category creation][]. + +All entities that have the same name will be merged during test generation. Both entities are of the `category` type. + +`_defaultCategory` sets three data fields: + +* `name` defines the category name as `simpleCategory` with a unique suffix. Example: `simpleCategory598742365`. +* `name_lwr` defines the category name in lowercase format with a unique suffix. Example: `simplecategory697543215`. +* `is_active` sets the enable category to `true`. + +`SimpleSubCategory` sets four data fields: + +* `name` that defines the category name with a unique suffix. Example: `SimpleSubCategory458712365`. +* `name_lwr` that defines the category name in lowercase format with a unique suffix. Example: `simplesubcategory753698741`. +* `is_active` sets the enable category to `true`. +* `include_in_menu` that sets the include in the menu to `true`. + +The following is an example of a call in test: + +```xml + +``` + + + +This action inputs data from the `name` of the `_defaultCategory` entity (for example, `simpleCategory598742365`) into the field with the locator defined in the selector of the `categoryNameInput` element of the `AdminCategoryBasicFieldSection`. + +## Reference + +### entities {#entities-tag} + +`` is an element that contains all `` elements. + +### entity {#entity-tag} + +`` is an element that contains `` elements. + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|optional|Name of the ``. +`type`|string|optional|Node containing the exact name of `` type. Used later to find specific Persistence Layer Model class. `type` in `` can be whatever the user wants; There are no constraints. It is important when persisting data, depending on the `type` given, as it will try to match a metadata definition with the operation being done. Example: A `myCustomer` entity with `type="customer"`, calling ``, will try to find a metadata entry with the following attributes: ``. + +`` may contain one or more [``][], [``][], [``][], or [``][] elements in any sequence. + +### data {#data-tag} + +`` is an element containing a data/value pair. + +Attributes|Type|Use|Description +---|---|---|--- +`key`|string|optional|Key attribute of data/value pair. +`unique`|enum: `"prefix"`, `"suffix"`|optional|Add suite or test wide unique sequence as "prefix" or "suffix" to the data value if specified. + +### var {#var-tag} + +`` is an element that can be used to grab a key value from another entity. For example, when creating a customer with the `` action, the server responds with the auto-incremented ID of that customer. Use `` to access that ID and use it in another data entity. + +Attributes|Type|Use|Description +---|---|---|--- +`key`|string|optional|Key attribute of this entity to assign a value to. +`entityType`|string|optional|Type attribute of referenced entity. +`entityKey`|string|optional|Key attribute of the referenced entity from which to get a value. +`unique`|--|--|*This attribute hasn't been implemented yet.* + +### requiredEntity {#requiredentity-tag} + +`` is an element that specifies the parent/child relationship between complex types. + +Example: You have customer address info. To specify that relationship: + +```xml + + ... + AddressEntity + ... + +``` + +Attributes|Type|Use|Description +---|---|---|--- +`type`|string|optional|Type attribute of ``. + +### array {#array-tag} + +`` is an element that contains a reference to an array of values. + +Example: + +```xml + + ... + + 7700 W Parmer Ln + Bld D + + ... + +``` + +Attributes|Type|Use|Description +---|---|---|--- +`key`|string|required|Key attribute of this entity in which to assign a value. + +`` may contain [``][] elements. + +### item {#item-tag} + +`` is an individual piece of data to be passed in as part of the parent `` type. + + +[``]: #array-tag +[``]: #data-tag +[``]: #item-tag +[``]: #requiredentity-tag +[``]: #var-tag +[Actions]: ./test/actions.md +[category creation]: http://docs.magento.com/m2/ce/user_guide/catalog/category-create.html +[Credentials]: ./credentials.md +[test actions]: ./test/actions.md#actions-returning-a-variable \ No newline at end of file diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 000000000..4b839ff6e --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,37 @@ +# Debugging + +Debugging within the Magento Functional Testing Framework is helpful in identifying test bugs by allowing you to pause execution so that you may: + +- Examine the page. +- Check returned data and other variables being used during run-time. + +This is straightforward to do once you create a basic Debug Configuration. + +## Prerequisites + +- [Xdebug][] +- PHPUnit configured for use in [PHPStorm][] + +## Creating Debug Configuration with PHPStorm + +1. If not already installed, download the Codeception Framework plugin for PHPStorm (`PhpStorm->Preferences->Plugins`). +1. Click `Edit Configurations` on the configuration dropdown. +1. Click `+` and select `Codeception` from the available types. +1. Change `Test Scope` to `Type` and select `functional` from the `Type:` dropdown. +1. Find the `Custom Working Directory` option and set the path to your `dev/tests/acceptance/` directory. + +If you get a warning `Path to Codeception for local machine is not configured.`: + +1. Click `Fix`, then `+`, and select `Codeception Local`. +1. Click `...` and locate `/vendor/bin/codecept` in your Magento installation folder. + +The easiest method of tagging a test for debugging is the following: + +- In your Debug configuration, locate `Test Runner options:` and set `--group testDebug`. +- When you want to debug a test you are working on, simply add `` to the annotations. Be sure to remove this after done debugging. + +Your Debug Configuration should now be able to run your test and pause execution on any breakpoints you have set in the generated `.php` file under the `_generated` folder. + + +[Xdebug]: https://xdebug.org/docs/install +[PHPStorm]: https://www.jetbrains.com/phpstorm/ diff --git a/docs/extending.md b/docs/extending.md new file mode 100644 index 000000000..666121986 --- /dev/null +++ b/docs/extending.md @@ -0,0 +1,369 @@ +# Extending + +There are cases when you need to create many tests that are very similar to each other. +For example, only one or two parameters (for example, URL) might vary between tests. +To avoid copy-pasting and to save some time the Magento Functional Testing Framework (MFTF) enables you to extend test components such as [test], [data], and [action group]. +You can create or update any component of the parent body in your new test/action group/entity. + +* A test starting with `` creates a test `SampleTest` that takes body of existing test `ParentTest` and adds to it the body of `SampleTest`. +* An action group starting with `` creates an action group based on the `ParentActionGroup`, but with the changes specified in `SampleActionGroup`. +* An entity starting with `` creates an entity `SampleEntity` that is equivalent to merging the `SampleEntity` with the `ParentEntity`. + +Specify needed variations for a parent object and produce a copy of the original that incorporates the specified changes (the "delta"). + +
+Unlike merging, the parent test (or action group) will still exist after the test generation. +
+ +## Extending tests + +### Update a test step + + + +__Use case__: Create two similar tests with different `url` (`"{{AdminCategoryPage.url}}"` and `"{{OtherCategoryPage.url}}"`) in a test step. + +> Test with "extends": + +```xml + + + + ... + + ...(several steps) + + ...(several steps) + + + + ... + + + + +``` + +> Test without "extends": + +```xml + + + + ... + + ...(several steps) + + ...(several steps) + + + + ... + + ...(several steps) + + ...(several steps) + + +``` + +### Add a test step + +__Use case__: Create two similar tests where the second test contains two additional steps: + +* `checkOption` before `click` (`stepKey="clickLogin"`) +* `seeInCurrentUrl` after `click` in the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) + +> Tests with "extends": + +```xml + + + + + + + + + + + + + +``` + +> Tests without "extends": + +```xml + + + + + + + + + + + + + + + + + + +``` + +### Update a test annotation + +__Use case__: Create two similar tests where the second one contains two additional actions in the `before` hook: + +* `checkOption` before `click` (`stepKey="clickLogin"`) +* `seeInCurrentUrl` after `click` in the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) + +> Tests with "extends": + +```xml + + + + + + + + + + + + + + + + + +``` + +> Tests without "extends": + +```xml + + + + + + + + + + + + + + + + + + + + + + +``` + +## Extending action groups + +Extend an [action group] to add or update [actions] in your module. + +### Update an action + +__Use case__: The `CountProductA` test counts the particular product. +Modify the action group to use another product. + +> Action groups with "extends": + +```xml + + + + + + + + {{count}} + grabProducts + + + + + + + +``` + +> Action groups without "extends": + +```xml + + + + + + + + {{count}} + grabProducts + + + + + + + + + + {{count}} + grabProducts + + + +``` + +### Add an action + +__Use case__: The `GetProductCount` action group returns the count of products. +Add a new test `VerifyProductCount` that asserts the count of products: + +> Action groups with "extends": + +```xml + + + + + + + + + + + + + + {{count}} + grabProducts + + + +``` + +> Action groups without "extends": + +```xml + + + + + + + + + + + + + + + + {{count}} + grabProducts + + + +``` + + + +## Extending data + +Extend data to reuse entities in your module. + +### Update a data entry + +__Use case__: Create an entity named `DivPanelGreen`, which is similar to the `DivPanel` entity, except that it is green. + +> Entities with "extends": + +```xml + + + Red + 80px + 100% + + + Green + + +``` + +> Entities without "extends": + +```xml + + + Red + 80px + 100% + + + Green + 80px + 100% + + +``` + +### Add a data entry + +__Use case__: Create an entity named `DivPanelGreen`, which is similar to the `DivPanel` entity, except that it has a specific panel color. + +> Entities with "extends": + +```xml + + + Red + 80px + 100% + + + #000000 + True + + +``` + +> Entities without "extends": + +```xml + + + Red + 80px + 100% + + + #000000 + 80px + 100% + True + + +``` + + +[test]: ./test.md +[data]: ./data.md +[action group]: ./test/action-groups.md +[actions]: ./test/actions.md \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 000000000..727dc3b18 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,325 @@ +# Getting started + +
+[Find your MFTF version][] of the MFTF. +The latest Magento 2.3 release supports MFTF 2.3.13. +The latest Magento 2.2 release supports MFTF 2.3.8. +
+ +## Prepare environment {#prepare-environment} + +Make sure that you have the following software installed and configured on your development environment: + +- [PHP version supported by the Magento instance under test][php] +- [Composer 1.3 or later][composer] +- [Java 1.8 or later][java] +- [Selenium Server Standalone 3.6 or later][selenium server] and [ChromeDriver 2.33 or later][chrome driver] or other webdriver in the same directory + +
+[PhpStorm] supports [Codeception test execution][], which is helpful when debugging. +
+## Install Magento {#install-magento} + +Use instructions below to install Magento. + +### Step 1. Clone the `magento2` source code repository {#clone-magento} + +```bash +git clone https://github.com/magento/magento2.git +``` + +or + +```bash +git clone git@github.com:magento/magento2.git +``` + +### Step 2. Install dependencies {#install-dependencies} + +Checkout the Magento version that you are going to test. + +```bash +cd magento2/ +``` + +```bash +git checkout 2.3-develop +``` + +Install the Magento application. + +```bash +composer install +``` + +## Prepare Magento {#prepare-magento} + +Configure the following settings in Magento as described below. + +### WYSIWYG settings {#wysiwyg-settings} + +A Selenium web driver cannot enter data to fields with WYSIWYG. + +To disable the WYSIWYG and enable the web driver to process these fields as simple text areas: + +1. Log in to the Magento Admin as an administrator. +2. Navigate to **Stores \> Configuration \> General \> Content Management**. +3. In the WYSIWYG Options section set the **Enable WYSIWYG Editor** option to **Disabled Completely**. +4. Click **Save Config**. + +
+When you want to test the WYSIWYG functionality, re-enable WYSIWYG in your test suite. +
+ +### Security settings {#security-settings} + +To enable the **Admin Account Sharing** setting, to avoid unpredictable logout during a testing session, and disable the **Add Secret Key in URLs** setting, to open pages using direct URLs: + +1. Navigate to **Stores \> Configuration \> Advanced \> Admin \> Security**. +2. Set **Admin Account Sharing** to **Yes**. +3. Set **Add Secret Key to URLs** to **No**. +4. Click **Save Config**. + +### Nginx settings {#nginx-settings} + +If Nginx Web server is used on your development environment then **Use Web Server Rewrites** setting in **Stores \> Configuration \> Web \> Search Engine Optimization** must be set to **Yes**. + +To be able to run Magento command line commands in tests add the following location block to Nginx configuration file: + +```conf +location ~* ^/dev/tests/acceptance/utils($|/) { + root $MAGE_ROOT; + location ~ ^/dev/tests/acceptance/utils/command.php { + fastcgi_pass fastcgi_backend; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } +} +``` +## Set up an embedded MFTF {#setup-framework} + +This is a default setup that you would need to start using the MFTF to cover your Magento project with functional testing. +It installs the framework using an existing Composer dependency such as `magento/magento2-functional-testing-framework`. +If you want to set up the MFTF as a standalone tool, refer to [Set up a standalone MFTF][]. + +Install the MFTF. + +```bash +composer install +``` + +### Step 1. Build the project {#build-project} + +In the Magento project root, run: + +```bash +vendor/bin/mftf build:project +``` + +If you use PhpStorm, generate a URN catalog: + +```bash +vendor/bin/mftf generate:urn-catalog .idea/ +``` + +If the file does not exist, add the `--force` option to create it: + +```bash +vendor/bin/mftf generate:urn-catalog --force .idea/ +``` + +See [`generate:urn-catalog`][] for more details.' + +
+You can simplify command entry by adding the absolute path to the `vendor/bin` directory path to your PATH environment variable. +After adding the path, you can run `mftf` without having to include `vendor/bin`. +
+ +### Step 2. Edit environmental settings {#environment-settings} + +In the `magento2/dev/tests/acceptance/` directory, edit the `.env` file to match your system. + +```bash +vim dev/tests/acceptance/.env +``` + +Specify the following parameters, which are required to launch tests: + +- `MAGENTO_BASE_URL` must contain a domain name of the Magento instance that will be tested. + Example: `MAGENTO_BASE_URL=http://magento.test` + +- `MAGENTO_BACKEND_NAME` must contain the relative path for the Admin area. + Example: `MAGENTO_BACKEND_NAME=admin` + +- `MAGENTO_ADMIN_USERNAME` must contain the username required for authorization in the Admin area. + Example: `MAGENTO_ADMIN_USERNAME=admin` + +- `MAGENTO_ADMIN_PASSWORD` must contain the user password required for authorization in the Admin area. + Example: `MAGENTO_ADMIN_PASSWORD=123123q` + +
+If the `MAGENTO_BASE_URL` contains a subdirectory like `http://magento.test/magento2ce`, specify `MAGENTO_CLI_COMMAND_PATH`. +
+ +Learn more about environmental settings in [Configuration][]. + +### Step 3. Enable the Magento CLI commands + +In the `magento2/dev/tests/acceptance` directory, run the following command to enable the MFTF to send Magento CLI commands to your Magento instance. + + ```bash +cp dev/tests/acceptance/.htaccess.sample dev/tests/acceptance/.htaccess +``` + +### Step 4. Generate and run tests {#run-tests} + +To run tests, you need a running Selenium server and [`mftf`][] commands. + +#### Run the Selenium server {#selenium-server} + +Run the Selenium server in terminal. +For example, the following commands run the Selenium server for Google Chrome: + +```bash +cd / +``` + +```bash +java -Dwebdriver.chrome.driver=chromedriver -jar selenium-server-standalone-3.14.0.jar +``` + +#### Generate and run all tests {#run-all-tests} + +```bash +vendor/bin/mftf generate:tests +``` + +```bash +cd dev/tests/acceptance +``` + +```bash +vendor/bin/codecept run functional -c dev/tests/acceptance/codeception.yml +``` + +See more commands in [`codecept`][]. + +#### Run a simple test {#run-test} + +To clean up the previously generated tests, and then generate and run a single test `AdminLoginTest`, run: + +```bash +vendor/bin/mftf run:test AdminLoginTest --remove +``` + +See more commands in [`mftf`][]. + +### Step 5. Generate reports {#reports} + +During testing, the MFTF generates test reports in CLI. +You can generate visual representations of the report data using [Allure Framework][]. +To view the reports in GUI: + +- [Install Allure][] +- Run the tool to serve the artifacts in `dev/tests/acceptance/tests/_output/allure-results/`: + +```bash +allure serve dev/tests/acceptance/tests/_output/allure-results/ +``` + +Learn more about Allure in the [official documentation][allure docs]. + +## Set up a standalone MFTF + +The MFTF is a root level Magento dependency, but it is also available for use as a standalone application. +You may want to use a standalone application when you develop for or contribute to MFTF, which facilitates debugging and tracking changes. +These guidelines demonstrate how to set up and run Magento acceptance functional tests using standalone MFTF. + +### Prerequisites + +This installation requires a local instance of the Magento application. +The MFTF uses the [tests from Magento modules][mftf tests] as well as the `app/autoload.php` file. + +### Step 1. Clone the MFTF repository + +If you develop or contribute to the MFTF, it makes sense to clone your fork of the MFTF repository. +For contribution guidelines, refer to the [Contribution Guidelines for the Magento Functional Testing Framework][contributing]. + +### Step 2. Install the MFTF + +```bash +cd magento2-functional-testing-framework +``` + +```bash +composer install +``` + +### Step 3. Build the project {#build-project} + +```bash +bin/mftf build:project +``` + +### Step 4. Edit environment settings + +In the `dev/.env` file, define the [basic configuration][] and [`MAGENTO_BP`][] parameters. + +### Step 5. Enable the Magento CLI commands {#add-cli-commands} + +Copy the `etc/config/command.php` file into your Magento installation at `/dev/tests/acceptance/utils/`. +Create the `utils/` directory, if you didn't find it. + +### Step 6. Remove the MFTF package dependency in Magento + +The MFTF uses the Magento `app/autoload.php` file to read Magento modules. +The MFTF dependency in Magento supersedes the standalone registered namespaces unless it is removed at a Composer level. + +```bash +composer remove magento/magento2-functional-testing-framework --dev -d +``` + +### Step 7. Run a simple test + +Generate and run a single test that will check your logging to the Magento Admin functionality: + +```bash +bin/mftf run:test AdminLoginTest +``` + +You can find the generated test at `dev/tests/functional/tests/MFTF/_generated/default/`. + +### Step 8. Generate Allure reports + +The standalone MFTF generates Allure reports at `dev/tests/_output/allure-results/`. +Run the Allure server pointing to this directory: + +```bash +allure serve dev/tests/_output/allure-results/ +``` + + + +[`codecept`]: commands/codeception.html +[`generate:urn-catalog`]: commands/mftf.html#generateurn-catalog +[`MAGENTO_BP`]: configuration.html#magento_bp +[`mftf`]: commands/mftf.html +[allure docs]: https://docs.qameta.io/allure/ +[Allure Framework]: http://allure.qatools.ru/ +[basic configuration]: configuration.html#basic-configuration +[build]: #build-project +[chrome driver]: https://sites.google.com/a/chromium.org/chromedriver/downloads +[Codeception Test execution]: https://blog.jetbrains.com/phpstorm/2017/03/codeception-support-comes-to-phpstorm-2017-1/ +[composer]: https://getcomposer.org/download/ +[Configuration]: configuration.html +[contributing]: https://github.com/magento/magento2-functional-testing-framework/blob/develop/.github/CONTRIBUTING.md +[install Allure]: https://github.com/allure-framework/allure2#download +[java]: http://www.oracle.com/technetwork/java/javase/downloads/index.html +[mftf tests]: introduction.html#mftf-tests +[php]: https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html#php +[PhpStorm]: https://www.jetbrains.com/phpstorm/ +[selenium server]: https://www.seleniumhq.org/download/ +[Set up a standalone MFTF]: #set-up-a-standalone-mftf +[test suite]: suite.html +[Find your MFTF version]: introduction.html#find-your-mftf-version diff --git a/docs/img/action-groups-dia.svg b/docs/img/action-groups-dia.svg new file mode 100644 index 000000000..853420d54 --- /dev/null +++ b/docs/img/action-groups-dia.svg @@ -0,0 +1,516 @@ + + + + + + image/svg+xml + + + + + + + + + + actionTypeTags + + + + + + + + + + + + argument + 0..∞ + + + + + + + + + + + + + + + + + + + arguments + + + + + + + + + + + + + + + + + + + + + + 0..∞ + + + + + + + + + + actionGroup + 1..∞ + + + + + + + + + + + actionGroups + + + + + + + + + + + + + + + + diff --git a/docs/img/catalogCategoryRepository-operations.png b/docs/img/catalogCategoryRepository-operations.png new file mode 100644 index 000000000..5db7b7008 Binary files /dev/null and b/docs/img/catalogCategoryRepository-operations.png differ diff --git a/docs/img/data-dia.svg b/docs/img/data-dia.svg new file mode 100644 index 000000000..fbaf7d8ec --- /dev/null +++ b/docs/img/data-dia.svg @@ -0,0 +1,489 @@ + + + + + + image/svg+xml + + + + + + + 0..∞ + + + + + data + + + 0..∞ + + + + + var + + + 0..∞ + + + + + requiredEntity + + + 0..∞ + + + + + item + + + + + + + + + + + + + + 0..∞ + + + + + array + + + + + + + + + + + + + + + + + + + + + + + 1..∞ + + + + + + + + + entity + + + 0..∞ + + + + + + + + + + + + + + + entities + + + diff --git a/docs/img/issue.png b/docs/img/issue.png new file mode 100644 index 000000000..8a2941f4c Binary files /dev/null and b/docs/img/issue.png differ diff --git a/docs/img/metadata-dia.svg b/docs/img/metadata-dia.svg new file mode 100644 index 000000000..0ce0fc27a --- /dev/null +++ b/docs/img/metadata-dia.svg @@ -0,0 +1,1050 @@ + + + + + + image/svg+xml + + + + + + + + + + + field + 0..∞ + + + + + + + array + 0..∞ + + + + + + + + + + + + object + 0..∞ + + + + + + + + + + + + + + + + + + + + + + 0..∞ + + + + + + + + + + field + 0..∞ + + + + + + value + + + + + + + object + 0..∞ + + + + + + + + + + + + + + + + + + + + + + + 0..∞ + + + + + + + + + + array + 0..∞ + + + + + + + + + header + + + 0..∞ + + + + + param + 0..∞ + + + + + + + + object + 0..∞ + + + + + + + + + + + + + + contentType + + + + + + + + + + + + + + + + + + + 0..∞ + + + + + + + + + + operation + 0..∞ + + + + + + + + + + + + + + + + + + + + + operations + + + + + diff --git a/docs/img/mftf-fork.gif b/docs/img/mftf-fork.gif new file mode 100644 index 000000000..650687b06 Binary files /dev/null and b/docs/img/mftf-fork.gif differ diff --git a/docs/img/page-dia.svg b/docs/img/page-dia.svg new file mode 100644 index 000000000..668891db0 --- /dev/null +++ b/docs/img/page-dia.svg @@ -0,0 +1,290 @@ + + + + + + image/svg+xml + + + + + + + + + + section + + 0..∞ + + + + + + + + + + + + + + + + + page + + 1..∞ + + + + + + + + + + + + + + + + + + + pages + + + + + diff --git a/docs/img/pull-request.png b/docs/img/pull-request.png new file mode 100644 index 000000000..66bff3f63 Binary files /dev/null and b/docs/img/pull-request.png differ diff --git a/docs/img/section-dia.svg b/docs/img/section-dia.svg new file mode 100644 index 000000000..1ee7611af --- /dev/null +++ b/docs/img/section-dia.svg @@ -0,0 +1,296 @@ + + + + + + image/svg+xml + + + + + + + + + + + element + + + 1..∞ + + + + + + + + + + + + + + + + + + + + + 1..∞ + + + + + + + + + + + + + + + + + + + + + sections + + + + + diff --git a/docs/img/switching-the-base.png b/docs/img/switching-the-base.png new file mode 100644 index 000000000..5b6653d36 Binary files /dev/null and b/docs/img/switching-the-base.png differ diff --git a/docs/img/test-dia.svg b/docs/img/test-dia.svg new file mode 100644 index 000000000..fcf2628da --- /dev/null +++ b/docs/img/test-dia.svg @@ -0,0 +1,1228 @@ + + + + + + image/svg+xml + + + + + + + + + testTypeTags + + + + + + + + + testTypeTags + + + + + + + + + + + + + + + + + + + + + 0..∞ + + + + + + + + + before + + + + + + + testTypeTags + + + + + + + + + + + + + + + + + + + + + 0..∞ + + + + + + + + + after + + + + + + + + annotations + + + + + + + + + + + + + + + + + + + + + + + + + 0..∞ + + + + + + + + + + test + 1..∞ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tests + + + + + + + + actionTypeTags + + + + + + + + + + + argument + + + + + + + + + + + + 0..∞ + + + + + + + + + actionGroup + + + + + + + + + + + + + + + + + + + + + + + + + + + testTypeTags + + + diff --git a/docs/img/trouble-chrome232.png b/docs/img/trouble-chrome232.png new file mode 100644 index 000000000..4af49146c Binary files /dev/null and b/docs/img/trouble-chrome232.png differ diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 000000000..1c7bfd329 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,136 @@ +# Introduction to the Magento Functional Testing Framework + +[Find your MFTF version][] of the MFTF. + +The Magento Functional Testing Framework (MFTF) aims to replace the [Functional Testing Framework] in future releases. +MFTF improves: + +- **Traceability** for clear logging and reporting capabilities. +- **Modularity** to run tests based on installed modules and extensions. +- **Customizability** for existing tests. +- **Readability** using clear and declarative XML test steps. +- **Maintainability** based on simple test creation and overall structure. + +Because MFTF tests are written in XML, you no longer need to learn PHP to write tests. + +
+We are actively developing functional tests. +Refer to `/app/code///Test/Mftf/` for examples. +Check out the [MFTF Test Migration][] repo. +
+ +## Audience + +This MFTF guide is intended for Magento developers and software engineers, such as QA specialists, PHP developers, and system integrators. + +## Goals + +The purpose of MFTF is to: + +- Facilitate functional testing and minimize the effort it takes to perform regression testing. +- Make it easier to support the extension and customization of tests via XML merging. + +## Scope + +MFTF will enable you to: + +- Test user interactions with web applications in testing. +- Write functional tests located in `/app/code///Test/Mftf/`. +- Cover basic functionality using out-of-the-box tests. You can test extended functionality using custom tests. +- Automate regression testing. + +## Use cases + +As a Magento developer, test changes, such as extended search functionality, a new form attribute, or new product tags. + +As a software engineer, perform regression testing before release to ensure that Magento works as expected with new functionality. + +## Find your MFTF version + +There are two options to find out your MFTF version: + +- using the MFTF CLI +- using the Composer CLI + +### MFTF CLI + +```bash +cd / +``` + +```bash +vendor/bin/mftf --version +``` + +### Composer CLI + +```bash +cd / +``` + +```bash +composer show magento/magento2-functional-testing-framework +``` + +## Contents of dev/tests/acceptance + +```tree +tests + _data // Additional files required for tests (e.g. pictures, CSV files for import/export, etc.) + _output // The directory is generated during test run. It contains testing reports. + _suite // Test suites. + _bootstrap.php // The script that executes essential initialization routines. + functional.suite.dist.yml // The Codeception functional test suite configuration (generated while running 'bin/mftf build:project') +utils // The test-running utilities. +.env.example // Example file for environmental settings. +.credentials.example // Example file for credentials to be used by the third party integrations (generated while running 'bin/mftf build:project'; should be filled with the appropriate credentials in the corresponding sandboxes). +.gitignore // List of files ignored by git. +.htaccess.sample // Access settings for the Apache web server to perform the Magento CLI commands. +codeception.dist.yml // Codeception configuration (generated while running 'bin/mftf build:project') +``` + +## MFTF output + +- Generated PHP Codeception tests +- Codeception results and console logs +- Screenshots and HTML failure report +- Allure formatted XML results +- Allure report dashboard of results + +## MFTF tests + +The MFTF supports two different locations for storing the tests and test artifacts: + +- `/app/code///Test/Mftf/` is the directory to create new tests. +- `/vendor///Test/Mftf/` is the directory with the out of the box tests (fetched by the Composer). + +All tests and test data from these locations are merged in the order indicated in the above list. + +The file structure under the both path cases is the same: + +```tree + +├── ActionGroup +│   └── ... +├── Data +│   └── ... +├── Metadata +│   └── ... +├── Page +│   └── ... +├── Section +│   └── ... +└── Test + └── ... +``` + +## MFTF on Github + +Follow the [MFTF project] and [contribute on Github]. + + +[contribute on Github]: https://github.com/magento/magento2-functional-testing-framework/blob/master/.github/CONTRIBUTING.md +[Functional Testing Framework]: https://devdocs.magento.com/guides/v2.3/mtf/mtf_introduction.html +[MFTF project]: https://github.com/magento/magento2-functional-testing-framework +[Find your MFTF version]: #find-your-mftf-version +[MFTF Test Migration]: https://github.com/magento/magento-functional-tests-migration \ No newline at end of file diff --git a/docs/merge_points/extend-action-groups.md b/docs/merge_points/extend-action-groups.md new file mode 100644 index 000000000..95d6cff90 --- /dev/null +++ b/docs/merge_points/extend-action-groups.md @@ -0,0 +1,102 @@ +# Extend action groups + +Extending an action group doesn't affect the existing action group. + +In this example we add a `` command to check the checkbox that our extension added with a new action group for the simple product creation form. + +## Starting action group + + + +```xml + + + + + + + + + + + + + + + + + + + + + + + +``` + +## File to merge + +```xml + + + + +``` + +## Resultant action group + +Note that there are now two action groups below. + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + + \ No newline at end of file diff --git a/docs/merge_points/extend-data.md b/docs/merge_points/extend-data.md new file mode 100644 index 000000000..344d11935 --- /dev/null +++ b/docs/merge_points/extend-data.md @@ -0,0 +1,70 @@ +# Extend data entities + +Extending an action group doesn't affect the existing action group. + +In this example we add a `` command to check the checkbox that our extension added with a new action group for the simple product creation form. + +## Starting entity + +```xml + + SimpleProduct + simple + 4 + SimpleProduct + 123.00 + 4 + 1 + 1000 + simpleproduct + 1 + EavStockItem + CustomAttributeCategoryIds + +``` + +## File to merge + +```xml + + + 1001 + dataHere + +``` + +## Resultant entity + +Note that there are now two data entities below. + +```xml + + SimpleProduct + simple + 4 + SimpleProduct + 123.00 + 4 + 1 + 1000 + simpleproduct + 1 + EavStockItem + CustomAttributeCategoryIds + + + SimpleProduct + simple + 4 + SimpleProduct + 123.00 + 4 + 1 + 1001 + simpleproduct + 1 + EavStockItem + CustomAttributeCategoryIds + dataHere + +``` \ No newline at end of file diff --git a/docs/merge_points/extend-tests.md b/docs/merge_points/extend-tests.md new file mode 100644 index 000000000..7398e273d --- /dev/null +++ b/docs/merge_points/extend-tests.md @@ -0,0 +1,145 @@ +# Extend tests + +Data objects can be merged to cover the needs of your extension. + +In this example, we add an action group to a new copy of the original test for our extension. + +## Starting test + +```xml + + + + + + <description value="Admin should be able to create a Simple Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-23414"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="simpleProduct" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> +</test> +``` + +## File to merge + +```xml +<test name="AdminCreateSimpleProductExtensionTest" extends="AdminCreateSimpleProductTest"> + <!-- Since this is its own test you need the annotations block --> + <annotations> + <features value="Catalog"/> + <stories value="Create a Simple Product via Admin"/> <!-- you should leave this the same since it is part of the same group --> + <title value="Admin should be able to create a Simple Product with my extension"/> + <description value="Admin should be able to create a Simple Product with my extension via the product grid"/> + <severity value="CRITICAL"/> + <testCaseId value="Extension/Github Issue Number"/> + <group value="product"/> + </annotations> + <!-- This will be added after the step "fillProductFieldsInAdmin" on line 20 in the above test. --> + <actionGroup ref="AddMyExtensionData" stepKey="extensionField" after="fillProductFieldsInAdmin"> + <argument name="extensionData" value="_myData"/> + </actionGroup> + + <!-- This will be added after the step "assertProductInStorefront2" on line 28 in the above test. --> + <actionGroup ref="AssertMyExtensionDataExists" stepKey="assertExtensionInformation" after="assertProductInStorefront2"> + <argument name="extensionData" value="_myData"/> + </actionGroup> +</test> +``` + +## Resultant test + +Note that there are now two tests below. + +```xml +<test name="AdminCreateSimpleProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create a Simple Product via Admin"/> + <title value="Admin should be able to create a Simple Product"/> + <description value="Admin should be able to create a Simple Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-23414"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="simpleProduct" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> +</test> +<test name="AdminCreateSimpleProductExtensionTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create a Simple Product via Admin"/> + <title value="Admin should be able to create a Simple Product with my extension"/> + <description value="Admin should be able to create a Simple Product with my extension via the product grid"/> + <severity value="CRITICAL"/> + <testCaseId value="Extension/Github Issue Number"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="simpleProduct" value="_defaultProduct"/> + </actionGroup> + + <actionGroup ref="AddMyExtensionData" stepKey="extensionField"> + <argument name="extensionData" value="_myData"/> + </actionGroup> + + <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + + <actionGroup ref="AssertMyExtensionDataExists" stepKey="assertExtensionInformation"> + <argument name="extensionData" value="_myData"/> + </actionGroup> +</test> +``` \ No newline at end of file diff --git a/docs/merge_points/introduction.md b/docs/merge_points/introduction.md new file mode 100644 index 000000000..af03dade6 --- /dev/null +++ b/docs/merge_points/introduction.md @@ -0,0 +1,39 @@ +# Merge Points for testing extensions in MFTF + +The Magento Functional Testing Framework (MFTF) allows great flexibility when writing XML tests for extensions. +All parts of tests can be used, reused, and merged to best suit your needs and cut down on needless duplication. + +Extension developers can utilitze these merge points to leverage existing tests and modify just the parts needed to test their extension. For instance, if your extension adds a form field to a Catalog admin page, you can modify the existing Catalog tests and add actions, data, etc as needed to test the custom field. +This topic shows how to merge and reuse test elements when testing extensions. + +## Merging + +Follow the links below for an example of how to merge: + +- [Merge Action Groups][] +- [Merge Data][] +- [Merge Pages][] +- [Merge Sections][] +- [Merge Tests][] + +## Extending + +Only Test, Action Group, and Data objects in the MFTF Framework can be extended. +Extending can be very useful for extension developers since it will not affect existing tests. + +Consult [when to use Extends][] to use extends when deciding whether to merge or extend. + +- [Extend Action Groups][] +- [Extend Data][] +- [Extend Tests][] + +<!-- Link definitions --> +[when to use Extends]: ../best-practices.md#when-to-use-extends +[Merge Action Groups]: merge-action-groups.md +[Merge Data]: merge-data.md +[Merge Pages]: merge-pages.md +[Merge Sections]: merge-sections.md +[Merge Tests]: merge-tests.md +[Extend Action Groups]: extend-action-groups.md +[Extend Data]: extend-data.md +[Extend Tests]: extend-tests.md \ No newline at end of file diff --git a/docs/merge_points/merge-action-groups.md b/docs/merge_points/merge-action-groups.md new file mode 100644 index 000000000..3a4f70ab1 --- /dev/null +++ b/docs/merge_points/merge-action-groups.md @@ -0,0 +1,78 @@ +# Merge action groups + +An action group is a set of individual actions working together as a group. +These action groups can be shared between tests and they also be modified to your needs. + +In this example we add a `<click>` command to check the checkbox that our extension adds to the simple product creation form. + +## Starting test + +<!-- {% raw %} --> + +```xml +<actionGroup name="FillAdminSimpleProductForm"> + <arguments> + <argument name="category"/> + <argument name="simpleProduct"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + <fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> + <seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> + <seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> + <seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> + <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> +</actionGroup> +``` + +## File to merge + +```xml +<actionGroup name="FillAdminSimpleProductForm"> + <!-- This will be added after the step "fillQuantity" in the above test. --> + <click selector="{{MyExtensionSection.myCheckbox}}" stepKey="clickMyCheckbox" after="fillQuantity"/> +</actionGroup> +``` + +## Resultant test + +```xml +<actionGroup name="FillAdminSimpleProductForm"> + <arguments> + <argument name="category"/> + <argument name="simpleProduct"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> + <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> + <fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <!-- Merged line here --> + <click selector="{{MyExtensionSection.myCheckbox}}" stepKey="clickMyCheckbox"/> + + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> + <seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> + <seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> + <seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> + <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> +</actionGroup> +``` + +<!-- {% endraw %} --> \ No newline at end of file diff --git a/docs/merge_points/merge-data.md b/docs/merge_points/merge-data.md new file mode 100644 index 000000000..b3342cc46 --- /dev/null +++ b/docs/merge_points/merge-data.md @@ -0,0 +1,56 @@ +# Merge data + +Data objects can be merged to cover the needs of your extension. + +In this example we update the `quantity` to `1001` and add a new piece of data relevant to our extension. This will affect all other tests that use this data. + +## Starting entity + +```xml +<entity name="SimpleProduct" type="product"> + <data key="sku" unique="suffix">SimpleProduct</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">123.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <data key="quantity">1000</data> + <data key="urlKey" unique="suffix">simpleproduct</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> +</entity> +``` + +## File to merge + +```xml +<entity name="SimpleProduct" type="product"> + <!-- myExtensionData will simply be added to the product and quantity will be changed to 1001. --> + <data key="quantity">1001</data> + <data key="myExtensionData">dataHere</data> +</entity> +``` + +## Resultant entity + +```xml +<entity name="SimpleProduct" type="product"> + <data key="sku" unique="suffix">SimpleProduct</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">SimpleProduct</data> + <data key="price">123.00</data> + <data key="visibility">4</data> + <data key="status">1</data> + <!-- Quantity updated --> + <data key="quantity">1001</data> + <data key="urlKey" unique="suffix">simpleproduct</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + <!-- Data key merged --> + <data key="myExtensionData">dataHere</data> +</entity> +``` \ No newline at end of file diff --git a/docs/merge_points/merge-pages.md b/docs/merge_points/merge-pages.md new file mode 100644 index 000000000..c7a3e8fa8 --- /dev/null +++ b/docs/merge_points/merge-pages.md @@ -0,0 +1,50 @@ +# Merge pages + +Sections can be merged into pages to cover your extension. + +In this example we add a section that may be relevant to our extension to the list of sections underneath one page. + +## Starting page + +```xml +<page name="AdminCategoryPage" url="catalog/category/" area="admin" module="Magento_Catalog"> + <section name="AdminCategorySidebarActionSection"/> + <section name="AdminCategoryMainActionsSection"/> + <section name="AdminCategorySidebarTreeSection"/> + <section name="AdminCategoryBasicFieldSection"/> + <section name="AdminCategorySEOSection"/> + <section name="AdminCategoryProductsSection"/> + <section name="AdminCategoryProductsGridSection"/> + <section name="AdminCategoryModalSection"/> + <section name="AdminCategoryMessagesSection"/> + <section name="AdminCategoryContentSection"/> +</page> +``` + +## File to merge + +```xml +<page name="AdminCategoryPage" url="catalog/category/" area="admin" module="Magento_Catalog"> + <!-- myExtensionSection will simply be added to the page --> + <section name="MyExtensionSection"/> +</page> +``` + +## Resultant page + +```xml +<page name="AdminCategoryPage"> + <section name="AdminCategorySidebarActionSection"/> + <section name="AdminCategoryMainActionsSection"/> + <section name="AdminCategorySidebarTreeSection"/> + <section name="AdminCategoryBasicFieldSection"/> + <section name="AdminCategorySEOSection"/> + <section name="AdminCategoryProductsSection"/> + <section name="AdminCategoryProductsGridSection"/> + <section name="AdminCategoryModalSection"/> + <section name="AdminCategoryMessagesSection"/> + <section name="AdminCategoryContentSection"/> + <!-- New section merged --> + <section name="MyExtensionSection"/> +</page> +``` \ No newline at end of file diff --git a/docs/merge_points/merge-sections.md b/docs/merge_points/merge-sections.md new file mode 100644 index 000000000..dbd6e3828 --- /dev/null +++ b/docs/merge_points/merge-sections.md @@ -0,0 +1,47 @@ +# Merge sections + +Sections can be merged together to cover your extension. + +In this example we add another selector to the section on the products page section. + +## Starting section + +<!-- {% raw %} --> + +```xml +<section name="ProductsPageSection"> + <element name="addProductButton" type="button" selector="//button[@id='add_new_product-button']"/> + <element name="checkboxForProduct" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> + <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']"/> + <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']"/> + <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> + <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> +</section> +``` + +## File to merge + +```xml +<section name="ProductsPageSection"> + <!-- myExtensionElement will simply be added to the page --> + <element name="myExtensionElement" type="button" selector="input.myExtension"/> +</section> +``` + +## Resultant section + +```xml +<section name="ProductsPageSection"> + <element name="addProductButton" type="button" selector="//button[@id='add_new_product-button']"/> + <element name="checkboxForProduct" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> + <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']"/> + <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']"/> + <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> + <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> + <!-- New element merged --> + <element name="myExtensionElement" type="button" selector="input.myExtension"/> +</section> +</page> +``` + +<!-- {% endraw %} --> \ No newline at end of file diff --git a/docs/merge_points/merge-tests.md b/docs/merge_points/merge-tests.md new file mode 100644 index 000000000..3958410e9 --- /dev/null +++ b/docs/merge_points/merge-tests.md @@ -0,0 +1,102 @@ +# Merge tests + +Tests can be merged to create a new test that covers new extension capabilities. + +In this example we add an action group that modifies the original test to interact with our extension sending in data we created. + +## Starting test + +```xml +<test name="AdminCreateSimpleProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create a Simple Product via Admin"/> + <title value="Admin should be able to create a Simple Product"/> + <description value="Admin should be able to create a Simple Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-23414"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="simpleProduct" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> +</test> +``` + +## File to merge + +```xml +<test name="AdminCreateSimpleProductTest"> + <!-- This will be added after the step "fillProductFieldsInAdmin" in the above test. --> + <actionGroup ref="AddMyExtensionData" stepKey="extensionField" after="fillProductFieldsInAdmin"> + <argument name="extensionData" value="_myData"/> + </actionGroup> + + <!-- This will be added after the step "assertProductInStorefront2" in the above test. --> + <actionGroup ref="AssertMyExtensionDataExists" stepKey="assertExtensionInformation" after="assertProductInStorefront2"> + <argument name="extensionData" value="_myData"/> + </actionGroup> +</test> +``` + +## Resultant test + +```xml +<test name="AdminCreateSimpleProductTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create a Simple Product via Admin"/> + <title value="Admin should be able to create a Simple Product"/> + <description value="Admin should be able to create a Simple Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-23414"/> + <group value="product"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + </before> + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="simpleProduct" value="_defaultProduct"/> + </actionGroup> + <!-- First merged action group --> + <actionGroup ref="AddMyExtensionData" stepKey="extensionField"> + <argument name="extensionData" value="_myData"/> + </actionGroup> + + <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> + <argument name="category" value="$$createPreReqCategory$$"/> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> + <argument name="product" value="_defaultProduct"/> + </actionGroup> + <!-- Second merged action group --> + <actionGroup ref="AssertMyExtensionDataExists" stepKey="assertExtensionInformation"> + <argument name="extensionData" value="_myData"/> + </actionGroup> +</test> +``` \ No newline at end of file diff --git a/docs/merging.md b/docs/merging.md new file mode 100644 index 000000000..32ea966c3 --- /dev/null +++ b/docs/merging.md @@ -0,0 +1,571 @@ +# Merging + +The MFTF allows you to merge test components defined in XML files, such as: + +- [`<tests>`][] +- [`<pages>`][] +- [`<sections>`][] +- [`<data>`][] +- `<action groups>` + +You can create, delete, or update the component. +It is useful for supporting rapid test creation for extensions and customizations. + +You can specify needed changes to an existing file and merge them to produce a modification of the original that incorporates the specified changes (the "delta"). + +Merging operates at the XML tag level, triggered by our parser when there are two (or more) entities with the same name. +Your update (XML node with changes) must have the same attribute `name` as its base node (the target object to be changed). + +For example: + +- All tests with `<test name="SampleTest>` will be merged into one. +- All pages with `<page name="SamplePage>` will be merged into one. +- All sections with `<section name="SampleAction">` will be merged into one. +- All data entities with `<entity name="sampleData" type="sample">` will be merged into one. +- All action groups with `<actionGroup name="selectNotLoggedInCustomerGroup">` will be merged into one. + +Although a file name does not influence merging, we recommend using the same file names in merging updates. +This makes it easier to search later on. + +## Add a test + +You cannot add another [`<test>`][tests] using merging functionality. +To add a `<test>`, create a new `*Test.xml` file or add a `<test>` node to an existing `*Test.xml` file. + +## Remove a test + +Tests cannot be removed while merging. +If a [`<test>`][tests] must be skipped due to a module completely invalidating a function, you can add the test to the `skip` group. + +Learn more about running tests with different options using [`mftf`] or [`codecept`] commands. + +### Example + +Skip the `AdminLoginTest` test in the `.../Backend/Test/AdminLoginTest.xml` file while merging with the `.../Foo/Test/AdminLoginTest.xml` file: + +<!-- {% raw %} --> + +```xml +<tests ...> + <test name="AdminLoginTest"> + <annotations> + <features value="Admin Login"/> + <stories value="Login on the Admin Login page"/> + <title value="You should be able to log into the Magento Admin backend."/> + <description value="You should be able to log into the Magento Admin backend."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-71572"/> + <group value="example"/> + <group value="login"/> + </annotations> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn"/> + <closeAdminNotification stepKey="closeAdminNotification"/> + <seeInCurrentUrl url="{{AdminLoginPage.url}}" stepKey="seeAdminLoginUrl"/> + </test> +</tests> +``` + +Create the `.../Foo/Test/AdminLoginTest.xml` file: + +```xml +<tests ...> + <test name="AdminLoginTest"> + <annotations> + <skip> + <issueId value="Issue#"/> + </skip> + </annotations> + </test> +</tests> +``` + +The `AdminLoginTest` result corresponds to: + +```xml +<test name="AdminLoginTest"> + <annotations> + <features value="Admin Login"/> + <stories value="Login on the Admin Login page"/> + <title value="You should be able to log into the Magento Admin backend."/> + <description value="You should be able to log into the Magento Admin backend."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-71572"/> + <group value="example"/> + <group value="login"/> + <skip> + <issueId value="Issue#"/> + </skip> + </annotations> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn"/> + <closeAdminNotification stepKey="closeAdminNotification"/> + <seeInCurrentUrl url="{{AdminLoginPage.url}}" stepKey="seeAdminLoginUrl"/> +</test> +``` + +## Update a test + +Any change must include information about where it should be inserted relative to other test steps in the scope of the test. + +This is done using the `before` or `after` element. +See the previous examples. + +### Add a test step + +**Use case**: Add `checkOption` before `click` (`stepKey="clickLogin"`) and add `seeInCurrentUrl` after the `click` in the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: + +```xml +<tests ...> + <test name="LogInAsAdminTest"> + <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> + </test> +</tests> +``` + +Create the `.../Foo/Test/LogInAsAdminTest.xml` file: + +```xml +<tests ...> + <test name="LogInAsAdminTest"> + <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe" before="clickLogin"/> + <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl" after="clickLogin"/> + </test> +</tests> +``` + +The `LogInAsAdminTest` result corresponds to: + +```xml +<test name="LogInAsAdminTest"> + <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> + <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl"/> + <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> +</test> +``` + +### Remove a test step + +**Use case**: Remove `see` (`stepKey="seeLifetimeSales"`) from the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: + +```xml +<tests ...> + <test name="LogInAsAdminTest"> + <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> + </test> +</tests> +``` + +Create the `.../Foo/Test/LogInAsAdminTest.xml` file: + +```xml +<tests ...> + <test name="LogInAsAdminTest"> + <remove keyForRemoval="seeLifetimeSales"/> + </test> +</tests> +``` + +The `LogInAsAdminTest` result corresponds to: + +```xml +<test name="LogInAsAdminTest"> + <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> +</test> +``` + +### Update a test step + +**Use case**: Change selector in `fillField` (`stepKey="fillPassword"`) of the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: + +```xml +<tests ...> + <test name="LogInAsAdminTest"> + <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> + </test> +</tests> +``` + +Create the `.../Foo/Test/LogInAsAdminTest.xml` file: + +```xml +<tests ...> + <test name="LogInAsAdminTest"> + <fillField selector="{{AdminLoginFormSection.wrong-password}}" userInput="password" stepKey="fillPassword"/> + </test> +</tests> +``` + +The `LogInAsAdminTest` result corresponds to: + +```xml +<test name="LogInAsAdminTest"> + <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.wrong-password}}" userInput="password" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> +</test> +``` + +### Add several test steps {#insert-after} + +**Use case**: Add several actions after the `click` (`stepKey="clickLogin"`) in the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: + +```xml +<tests ...> + <test name="LogInAsAdminTest"> + <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> + </test> +</tests> +``` + +Create the `.../Foo/Test/LogInAsAdminTest.xml` file: + +```xml +<tests ...> + <test name="LogInAsAdminTest" insertAfter="clickLogin"> + <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe"/> + <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl"/> + </test> +</tests> +``` + +The `LogInAsAdminTest` result corresponds to: + +```xml +<test name="LogInAsAdminTest"> + <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> + <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> + <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> + <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> + <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe"/> + <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl"/> + <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> +</test> +``` + +## Merge action groups + +Merging action groups allows you to extend existing tests by reusing existing action groups, while customizing them for your specific needs. + +### Use case + +Here is an action group for selecting `customerGroup` in the `Cart Price Rules` section. +The controls change drastically in the B2B version, so it was abstracted to an action group so that it may be easily changed if B2B is enabled. + +> Action group for selecting `customerGroup` in the `Cart Price Rules` section: + +```xml +<actionGroup name="selectNotLoggedInCustomerGroup"> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> +</actionGroup> +``` + +> B2B Merge file + +```xml +<!-- name matches --> +<actionGroup name="selectNotLoggedInCustomerGroup"> + <!-- removes the original action --> + <remove keyForRemoval="selectCustomerGroup"/> + <!-- adds in sequence of actions to be performed instead--> + <click selector="{{AdminCartPriceRulesFormSection.customerGroups}}" stepKey="expandCustomerGroups"/> + <fillField selector="{{AdminCartPriceRulesFormSection.customerGroupsInput}}" userInput="NOT LOGGED IN" stepKey="fillCustomerGroups"/> + <click selector="{{AdminCartPriceRulesFormSection.customerGroupsFirstResult}}" stepKey="selectNotLoggedInGroup"/> + <click selector="{{AdminCartPriceRulesFormSection.customerGroupsDoneBtn}}" stepKey="closeMultiSelect"/> +</actionGroup> +``` + +## Merge pages + +Use [page] merging to add or remove [sections] in your module. + +To merge [pages][page], the `page name` must be the same as in the base module. +`page name` is set in the `module` attribute. + +### Add a section + +**Use case**: The `FooBackend` module extends the `Backend` module and requires use of two new sections that must be covered with tests. +Add `BaseBackendSection` and `AnotherBackendSection` to the `BaseBackendPage` (`.../Backend/Page/BaseBackendPage.xml` file): + +```xml +<pages ...> + <page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> + <section name="BaseBackendSection"/> + <section name="AnotherBackendSection"/> + </page> +</pages> +``` + +Create the `.../FooBackend/Page/BaseBackendPage.xml` file: + +```xml +<pages ...> + <page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> + <section name="NewExtensionSection"/> + </page> +</pages> +``` + +The `BaseBackendPage` result corresponds to: + +```xml +<page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> + + <section name="BaseBackendSection"/> + <section name="AnotherBackendSection"/> + <section name="NewExtensionSection"/> +</page> +``` + +### Remove a section + +**Use case**: The `FooBackend` module extends the `Backend` module and requires deletion of the `AnotherBackendSection` section (the `.../Backend/Page/BaseBackendPage.xml` file): + +```xml +<page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> + <section name="BaseBackendSection"/> + <section name="AnotherBackendSection"/> +</page> +``` + +Create the `.../FooBackend/Page/BaseBackendPage.xml` file: + +```xml +<page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> + <section name="AnotherBackendSection" remove="true"/> +</page> +``` + +The `BaseBackendPage` result corresponds to: + +```xml +<page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> + <section name="BaseBackendSection"/> +</page> +``` + +## Merge sections + +Use merging to add, remove, or update [elements] in sections. + +All sections with the same _file name_, _section name_, and _element name_ are merged during test generation. + +### Add an element + +**Use case**: The `FooBackend` module extends the `Backend` module and requires a new `mergeElement` element in the `AdminLoginFormSection`. +Add `mergeElement` to the `AdminLoginFormSection`: + +```xml +<sections ...> + <section name="AdminLoginFormSection"> + <element name="username" type="input" selector="#username"/> + <element name="password" type="input" selector="#login"/> + <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> + </section> +</sections> +``` + +Create the `.../FooBackend/Section/AdminLoginFormSection.xml` file: + +```xml +<sections ...> + <section name="AdminLoginFormSection"> + <element name="mergeElement" type="input" selector="#selector"/> + </section> +</sections> +``` + +The `AdminLoginFormSection` result corresponds to: + +```xml +<section name="AdminLoginFormSection"> + <element name="username" type="input" selector="#username"/> + <element name="password" type="input" selector="#login"/> + <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> + <element name="mergeElement" type="input" selector="#selector"/> +</section> +``` + +### Remove an element + +**Use case**: The `FooBackend` module extends the `Backend` module and requires deletion of `username` in the `AdminLoginFormSection`. +Remove `username` from the `AdminLoginFormSection`: + +```xml +<sections ...> + <section name="AdminLoginFormSection"> + <element name="username" type="input" selector="#username"/> + <element name="password" type="input" selector="#login"/> + <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> + </section> +</sections> +``` + +Create the `.../FooBackend/Section/AdminLoginFormSection.xml` file: + +```xml +<sections ...> + <section name="AdminLoginFormSection"> + <element name="username" type="input" remove="true"/> + </section> +</sections> +``` + +The `AdminLoginFormSection` result corresponds to: + +```xml +<section name="AdminLoginFormSection"> + <element name="password" type="input" selector="#login"/> + <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> +</section> +``` + +### Update an element + +**Use case**: The `FooBackend` module extends the `Backend` module and requires change of a selector in `username` in the `AdminLoginFormSection`. +Update `username` in the `AdminLoginFormSection` (the `.../Backend/Section/AdminLoginFormSection.xml` file): + +```xml +<sections ...> + <section name="AdminLoginFormSection"> + <element name="username" type="input" selector="#username"/> + <element name="password" type="input" selector="#login"/> + <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> + </section> +</sections> +``` + +Create the `.../FooBackend/Section/AdminLoginFormSection.xml` file: + +```xml +<sections ...> + <section name="AdminLoginFormSection"> + <element name="username" type="input" selector="#newSelector"/> + </section> +</sections> +``` + +The `AdminLoginFormSection` result corresponds to: + +```xml +<section name="AdminLoginFormSection"> + <element name="username" type="input" selector="#newSelector"/> + <element name="password" type="input" selector="#login"/> + <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> +</section> +``` + +## Merge data + +You can add or update `<data>` elements within an `<entity>`. +Removal of individual `<data>` tags is not supported. + +Entities and data with the same _file name_, _entity name and type_, and _data key_ are merged during test generation. + +### Add data + +**Use case**: The `FooSample` module extends the `Sample` module and requires a new data item `thirdField` in the `_defaultSample` entity. +Add `<data key="thirdField">field3</data>` to the `_defaultSample` (the `.../Sample/Data/SampleData.xml` file): + +```xml +<entities ...> + <entity name="_defaultSample" type="testData"> + <data key="firstField">field1</data> + <data key="secondField">field2</data> + </entity> +</entities> +``` + +Create the `.../FooSample/Data/SampleData.xml` file: + +```xml +<entities ...> + <entity name="sampleData" type="testData"> + <data key="thirdField">field3</data> + </entity> +</entities> +``` + +The `_defaultSample` result corresponds to: + +```xml +<entity name="_defaultSample" type="testData"> + <data key="firstField">field1</data> + <data key="secondField">field2</data> + <data key="thirdField">field3</data> +</entity> +``` + +### Update data + +**Use case**: The `FooSample` module extends the `Sample` module and requires changing the `firstField` data item in the `_defaultSample` entity. +Change `firstField` to `<data key="firstField">overrideField</data>` in the `_defaultSample` (the `.../Sample/Data/SampleData.xml` file): + +```xml +<entities ...> + <entity name="_defaultSample" type="testData"> + <data key="firstField">field1</data> + <data key="secondField">field2</data> + </entity> +</entity> +``` + +Create the `.../FooSample/Data/SampleData.xml` file: + +```xml +<entities ...> + <entity name="_defaultSample" type="testData"> + <data key="firstField">overrideField</data> + </entity> +</entity> +``` + +The `_defaultSample` results corresponds to: + +```xml +<entity name="_defaultSample" type="testData"> + <data key="firstField">overrideField</data> + <data key="secondField">field2</data> +</entity> +``` + +<!-- {% endraw %} --> + +<!-- Link definitions --> + +[`codecept`]: ./commands/codeception.md +[`mftf`]: ./commands/mftf.md +[`<data>`]: ./data.md +[elements]: ./section.md#element-tag +[`<pages>`]: ./page.md +[`<sections>`]: ./section.md +[`<tests>`]: ./test.md diff --git a/docs/metadata.md b/docs/metadata.md new file mode 100644 index 000000000..87f2da7e0 --- /dev/null +++ b/docs/metadata.md @@ -0,0 +1,584 @@ +# Metadata + +In this topic we talk about handling entities that you need in your tests (such as categories, products, wish lists, and similar) using the MFTF. +Using data handling actions like [`createData`], [`deleteData`], [`updateData`], and [`getData`], you are able to create, delete, update, and read entities for your tests. +The framework enables you to send HTTP requests with these statically defined data entities: + +- [Sending a REST API request][rest request] +- [Handling a REST API response][rest response] +- [Sending an HTML form encoded in URL][html form] + +You have probably noticed that some modules in acceptance functional tests contain a directory, which is called `Metadata`. + +Example of a module with _Metadata_: + +```tree +Wishlist +├── Data +├── Metadata +├── Page +├── Section +└── Test +``` + +This directory contains XML files with metadata required to create a valid request to handle an entity defined in `dataType`. +A metadata file contains a list of operations with different types (defined in `type`). +Each [operation] includes: + +- The set of adjustments for processing a request in [attributes][operation], and in some cases, a response (see `successRegex` and `returnRegex` in [reference details][operation]). +- The type of body content encoding in [contentType]. +- The body of the request represented as a tree of objects, arrays, and fields. + +When a test step requires handling the specified data entity, the MFTF performs the following steps: + +- Reads input data (`<data/>`) and the type (the `type` attribute) of the specified [entity]. +- Searches the metadata operation for the `dataType` that matches the entity's `type`. For example, `<entity type="product">` matches `<operation dataType="product"`. +- Forms a request of the operation and the input data of the entity according to matching metadata. +- Stores a response and provides access to its data using MFTF variables syntax in XML. + +The following diagram demonstrates the XML structure of a metadata file: +![Structure of metadata](img/metadata-dia.svg) + +## Format {#format} + +```xml +<?xml version="1.0" encoding="UTF-8"?> +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="" + dataType="" + type="" + auth="" + url="" + method=""> + <contentType></contentType> + <object key="" dataType=""> + <field key="">integer</field> + <field key="">string</field> + <field key="">boolean</field> + <array key=""> + <value>string</value> + </array> + </object> + </operation> +``` + +## Principles {#principles} + +1. A `dataType` value must match the `type` value of the corresponding entity. +2. A file name should contain data type split with `_` and must end with `-meta`. + Example: `product_attribute-meta.xml`. +3. A metadata file may contain different types of operations (`type`) with the same data entity (`dataType`). + +Example: + + ```xml + <operations> + <operation type="create" dataType="category"> + ... + </operation> + <operation type="update" dataType="category"> + ... + </operation> + <operation type="delete" dataType="category"> + ... + </operation> + <operation type="get" dataType="category"> + ... + </operation> + </operations> + ``` + +## Handling entities using REST API {#handling-with-api} + +### Sending a REST API request + +The MFTF allows you to handle basic CRUD operations with an object using [Magento REST API][api reference] requests. +To convert a request to the MFTF format, wrap the corresponding REST API request into XML tags according to the [Reference documentation][reference]. + +- GET is used for retrieving data from objects. +- POST is used for creating new objects. +- PUT is used for updating objects. +- DELETE is used for deleting objects. + +This is an example of how to handle a category using REST API operations provided by the `catalogCategoryRepositoryV1` service. + +![REST API operations provided by catalogCategoryRepositoryV1][catalogCategoryRepositoryV1 image] + +The above screenshot from the [Magento REST API Reference][api reference] demonstrates a list of available operations to: + +- Delete a category by its identifier (`method="DELETE"`) +- Get information about a category by its ID (`method="GET"`) +- [Create a new category] (`method="POST"`) +- Update category data by its ID (`method="PUT"`) + +We assume that our `.env` file sets `MAGENTO_BASE_URL=https://example.com/` and `MAGENTO_BACKEND_NAME=admin`. + +#### Create a simple category {#create-object-as-adminOauth} + +Let's see what happens when you create a category: + +```xml +<createData entity="_defaultCategory" stepKey="createPreReqCategory"/> +``` + +The MFTF searches in the _Data_ directory an entity with `<entity name="_defaultCategory">` and reads `type` of the entity. +If there are more than one entity with the same name, all of the entities are merged. + +_Catalog/Data/CategoryData.xml_: + +```xml +<entity name="_defaultCategory" type="category"> + <data key="name" unique="suffix">simpleCategory</data> + <data key="name_lwr" unique="suffix">simplecategory</data> + <data key="is_active">true</data> +</entity> +``` + +Here, `type` is equal to `"category"`, which instructs the MFTF to search an operation with `dataType="category"`. +Since the action is __to create__ a category, the MFTF will also search for operation with `type="create"` in _Metadata_ for `dataType="category"`. + +_Catalog/Metadata/category-meta.xml_: + +```xml +<operation name="CreateCategory" dataType="category" type="create" auth="adminOauth" url="/V1/categories" method="POST"> + <contentType>application/json</contentType> + <object key="category" dataType="category"> + <field key="parent_id">integer</field> + <field key="name">string</field> + <field key="is_active">boolean</field> + <field key="position">integer</field> + <field key="level">integer</field> + <field key="children">string</field> + <field key="created_at">string</field> + <field key="updated_at">string</field> + <field key="path">string</field> + <field key="include_in_menu">boolean</field> + <array key="available_sort_by"> + <value>string</value> + </array> + <field key="extension_attributes">empty_extension_attribute</field> + <array key="custom_attributes"> + <value>custom_attribute</value> + </array> + </object> +</operation> +``` + +(The corresponding operation is provided by _catalogCategoryRepositoryV1_ in [REST API][api reference].) + +The following is encoded in `<operation>`: + +- `name="CreateCategory"` defines a descriptive name of the operation, which is used for merging if needed. +- `dataType="category"` defines a relation with data entities with input data for a Category (`<entity type="category">`). +- `auth="adminOauth"` defines OAuth authorization, which is required for the Admin area. +- `url="/V1/categories"` defines a routing URL to the corresponding service class. + (The request will be sent to `https://example.com/rest/V1/categories` if `MAGENTO_BASE_URL=https://example.com/` and `MAGENTO_BACKEND_NAME=admin` are set in the _acceptance/.env_ configuration file.) +- `method="POST"` defines a POST method of the HTTP request. + +`<contentType>application/json</contentType>` defines a content type of the REST API request, which is set as `application/json` here. + +The parameter that declares a body of the request is _catalogCategoryRepositoryV1SavePostBody_. +Using the [Reference], we can trace how the JSON request was converted into XML representation. + +<div class="bs-callout bs-callout-info"> +Comments in the example below are used to demonstrate relation between JSON request and MFTF metadata in XML. +JSON does not support comments. +</div> + +Model schema for _catalogCategoryRepositoryV1SavePostBody_ with XML representation of _Catalog/Metadata/category-meta.xml_ in comments: + +```json +{ // XML representation in the MFTF metadata format (see 'Catalog/Metadata/category-meta.xml') + "category": { // <object key="category" dataType="category"> + "id": 0, // Skipped, because Category ID is not available on UI when you create a new category. + "parent_id": 0, // <field key="parent_id">integer</field> + "name": "string", // <field key="name">string</field> + "is_active": true, // <field key="is_active">boolean</field> + "position": 0, // <field key="position">integer</field> + "level": 0, // <field key="level">integer</field> + "children": "string", // <field key="children">string</field> + "created_at": "string", // <field key="created_at">string</field> + "updated_at": "string", // <field key="updated_at">string</field> + "path": "string", // <field key="path">string</field> + "available_sort_by": [ // <array key="available_sort_by"> + "string" // <value>string</value> + ], // </array> + "include_in_menu": true, // <field key="include_in_menu">boolean</field> + "extension_attributes": {}, // <field key="extension_attributes">empty_extension_attribute</field>, where 'empty_extension_attribute' is a reference to operation with 'dataType="empty_extension_attribute"' (see 'Catalog/Metadata/empty_extension_attribute-meta.xml') + "custom_attributes": [ // <array key="custom_attributes"> + { // <value>custom_attribute</value>, where 'custom_attribute' is a reference to operation with 'dataType="custom_attribute"' (see 'Catalog/Metadata/custom_attribute-meta.xml') + "attribute_code": "string", + "value": "string" + } + ] // </array> + } // </object> +} +``` + +So, the body of a REST API request that creates a simple category is the following: + +```json +{ // XML representation of input data used to create a simple category ("Catalog/Data/CategoryData.xml") + "category": { // <entity name="_defaultCategory" type="category"> + "name": "simpleCategory_0986576456", // <data key="name" unique="suffix">simpleCategory</data> + "is_active": true // <data key="is_active">true</data> + } // </entity> +} +``` + +#### Create an object as a guest {#create-object-as-anonymous} + +The corresponding test step is: + +```xml +<createData entity="guestCart" stepKey="createGuestCart"/> +``` + +The MFTF searches in the _Data_ directory an entity with `<entity name="guestCart">` and reads `type`. + +_Quote/Data/GuestCartData.xml_: + +```xml +<entity name="guestCart" type="guestCart"> +</entity> +``` + +`type="guestCart"` points to the operation with `dataType=guestCart"` and `type="create"` in the _Metadata_ directory. + +_Catalog/Data/CategoryData.xml_: + +```xml +<operation name="CreateGuestCart" dataType="guestCart" type="create" auth="anonymous" url="/V1/guest-carts" method="POST"> + <contentType>application/json</contentType> +</operation> +``` + +As a result, the MFTF sends an unauthorized POST request with an empty body to the `https://example.com/rest/V1/guest-carts` and stores the single string response that the endpoint returns. + +### Handling a REST API response {#rest-response} + +There are cases when you need to reuse the data that Magento responded with to your POST request. + +Let's see how to handle data after you created a category with custom attributes: + +```xml +<createData entity="customizedCategory" stepKey="createPreReqCategory"/> +``` + +The MFTF receives the corresponding JSON response and enables you to reference its data using a variable of format: + +**$** _stepKey_ **.** _JsonKey_ **$** + +Example: + +```xml +$createPreReqCategory.id$ +``` + +And for a custom attribute: + +**$** _stepKey_ **.custom_attributes[** _attribute key_ **]$** + +Example: + +```xml +$createPreReqCategory.custom_attributes[is_anchor]$ +``` + +The following example of response in JSON demonstrates how to reference data on the root level and as data in custom attributes: + +```json +{ + "id": 7, //$createPreReqCategory.id$ + "parent_id": 2, //$createPreReqCategory.parent_id$ + "name": "simpleCategory_0986576456", //$createPreReqCategory.is_active$ + "is_active": true, + "position": 5, + "level": 2, + "children": "", + "created_at": "2018-05-08 14:27:18", + "updated_at": "2018-05-08 14:27:18", + "path": "1/2/7", + "available_sort_by": [], + "include_in_menu": true, + "custom_attributes": [ + { + "attribute_code": "is_anchor", + "value": "1" //$createPreReqCategory.custom_attributes[is_anchor]$ + }, + { + "attribute_code": "path", + "value": "1/2/7" //$createPreReqCategory.custom_attributes[path]$ + }, + { + "attribute_code": "children_count", + "value": "0" + }, + { + "attribute_code": "url_key", + "value": "simplecategory5af1b41cd58fb4" //$createPreReqCategory.custom_attributes[url_key]$ + }, + { + "attribute_code": "url_path", + "value": "simplecategory5af1b41cd58fb4" + } + ], +} +} +``` + +## Handling entities using HTML forms {#using-html-forms} + +For cases when REST API is not applicable, you may use [HTML forms] (when all object parameters are encoded in a URL as `key=name` attributes). +There are two different attributes to split access to different areas: + +- `auth="adminFormKey"` is used for objects in an Admin area. +- `auth="customerFormKey"` is used for objects in a storefront. + +You are able to create assurances with `successRegex`, and even return values with `returnRegex`. + +### Create an object in Admin {#create-object-as-adminFormKey} + +The `CreateStoreGroup` operation is used to persist a store group: + +Source file is _Store/Metadata/store_group-meta.xml_: + +```xml +<operation name="CreateStoreGroup" dataType="group" type="create" auth="adminFormKey" url="/admin/system_store/save" method="POST" successRegex="/messages-message-success/" > + <contentType>application/x-www-form-urlencoded</contentType> + <object dataType="group" key="group"> + <field key="group_id">string</field> + <field key="name">string</field> + <field key="code">string</field> + <field key="root_category_id">integer</field> + <field key="website_id">integer</field> + </object> + <field key="store_action">string</field> + <field key="store_type">string</field> +</operation> +``` + +The operation is called when `<createData>` (`type="create"`) points to a data entity of type `"group"` (`dataType="group"`). +It sends a POST request (`method="POST"`) to `http://example.com/admin/system_store/save` (`url="/admin/system_store/save"`) that is authorized for the Admin area (`auth="adminFormKey"`). +The request contains HTML form data encoded in the [application/x-www-form-urlencoded] content type (`<contentType>application/x-www-form-urlencoded</contentType>`). +If the returned HTML code contains the `messages-message-success` string, it is resolved as successful. + +The operation enables you to assign the following form fields: + +- `group/group_id` +- `group/name` +- `group/code` +- `group/root_category_id` +- `group/website_id` +- `store_action` +- `store_type` + +### Create an object in storefront {#create-object-as-customerFormKey} + +The MFTF uses the `CreateWishlist` operation to create a wish list on storefront: + +Source file is _Wishlist/Metadata/wishlist-meta.xml_ + +```xml +<operation name="CreateWishlist" dataType="wishlist" type="create" auth="customerFormKey" url="/wishlist/index/add/" method="POST" successRegex="" returnRegex="~\/wishlist_id\/(\d*?)\/~" > + <contentType>application/x-www-form-urlencoded</contentType> + <field key="product">integer</field> + <field key="customer_email">string</field> + <field key="customer_password">string</field> +</operation> +``` + +The operation is used when `<createData>` (`type="create"`) points to a data entity of type `"wishlist"` (`dataType="wishlist"`). +It sends a POST request (`method="POST"`) to `http://example.com/wishlist/index/add/` (`url="wishlist/index/add/"`) as a customer (`auth="customerFormKey"`). +The request contains HTML form data encoded in the [application/x-www-form-urlencoded] content type (`<contentType>application/x-www-form-urlencoded</contentType>`). +If the returned HTML code contains a string that matches the regular expression `~\/wishlist_id\/(\d*?)\/~`, it returns the matching value. + +The operation assigns three form fields: + +- `product` +- `customer_email` +- `customer_password` + +## Reference + +### operations {#operations-tag} + +Root element that points to the corresponding XML Schema. + +### operation {#operation-tag} + +| Attribute | Type | Use | Description | +|-----------------|------------------------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------| +| `name` | string | optional | Name of the operation. | +| `dataType` | string | required | Data type of the operation. It refers to a `type` attribute of data entity that will be used as a source of input data. | +| `type` | Possible values: `create`, `delete`, `update`, `get`. | required | Type of operation. | +| \*`url` | string | optional | A routing URL of the operation. Example value: `"/V1/categories"`. | +| \*\*`auth` | Possible values: `adminOath`, `adminFormKey`, `customerFormKey`, `anonymous` | optional | Type of authorization of the operation. | +| `method` | string | optional | HTTP method of the operation. Possible values: `POST`, `DELETE`, `PUT`, `GET`. | +| `successRegex` | string | optional | Determines if the operation was successful. Parses the HTML body in response and asserts if the value assigned to the `successRegex` exists. | +| `returnRegex` | string | optional | Determines if the response contains the matching value to return. | +| `removeBackend` | boolean | optional | Removes backend name from requested URL. Applicable when `auth="adminFormKey"`. | +| `filename` | string | optional | | + +- \*`url` - full URL is a concatenation of _ENV.baseUrl_ + `/rest/` + _url_. + To reuse data of a required entity or returned response use a field key wrapped in curly braces such as `{sku}`. + When the data to reuse is of a different type, declare also the type of data such as `{product.sku}`. + Example: `"/V1/products/{product.sku}/media/{id}"`. + +- \*\*`auth` - available values: + + - `adminOath` is used for REST API persistence in the Admin area with [OAuth-based authentication][oauth]. + - `adminFormKey` is used for HTML form persistence in the Admin area. + - `customerFormKey` is used for HTML form persistence in the Customer area. + - `anonymous` is used for REST API persistence without authorization. + +### contentType {#contentType-tag} + +Sets one of the following operation types: + +- `application/json` is used for REST API operations. +- `application/x-www-form-urlencoded` is used for HTML form operations. + +### object {#object-tag} + +Representation of a complex entity that may contain fields, arrays, and objects. +An object must match the [entity] of the same `type`. + +| Attribute | Type | Use | Description | +| ---------- | ------- | -------- | ---------------------------------------------------------------------------------------------- | +| `key` | string | optional | Name of the object. | +| `dataType` | string | required | Type of the related [entity]. | +| `required` | boolean | optional | Determines if the object is required or not. It must match the Magento REST API specification. | + +### field {#field-tag} + +Representation of HTML form or REST API fields. + +| Attribute | Type | Use | Description | +| ---------- | ------- | -------- | --------------------------------------------------------------------------------------------- | +| `key` | string | required | Name of the field. It must match the field name of the related [entity]. | +| `type` | string | optional | Type of the value. It may contain a primitive type or the type of another operation. | +| `required` | boolean | optional | Determines if the field is required or not. It must match the Magento REST API specification. | + +### array {#array-tag} + +Representation of an array. + +| Attribute | Type | Use | Description | +| --------- | ------ | -------- | ------------------ | +| `key` | string | required | Name of the array. | + +It contains one or more `value` elements. + +### value {#value-tag} + +Declares a data type for items within `<array>`. + +#### Example of an array with value of a primitive data type + +Metadata declaration of the operation schema: + +```xml +<array key="tax_rate_ids"> + <value>integer</value> +</array> +``` + +The value can contain one or more items. + +Data entity with the corresponding assignment: + +```xml +<array key="tax_rate_ids"> + <item>1</item> + <item>2</item> +</array> +``` + +- Resulted JSON request: + +```json +"tax_rate_ids": + [ + "item": 1, + "item": 2 + ] +``` + +#### Example of an array containing data entities + +```xml +<array key="product_options"> + <value>product_option</value> +</array> +``` + +The value declares the `product_options` array that contains one or more entities of the `product_option` data type. + +#### Example of an array containing a particular data field + +```xml +<array key="tax_rate_ids"> + <value>tax_rate.id</value> +</array> +``` + +The value declares the `tax_rate_ids` array that contains one or more `id` fields of the `tax_rate` data type entity. + +### header {#header-tag} + +An additional parameter in REST API request. + +| Attribute | Type | Use | Description | +| --------- | ------ | -------- | ---------------------------- | +| `param` | string | required | A REST API header parameter. | + +```xml +<header param="status">available</header> +``` + +### param {#param-tag} + +An additional parameter in URL. + +| Attribute | Type | Use | Description | +| --------- | ------ | -------- | -------------------------- | +| `key` | string | required | Name of the URL parameter. | + +Example: + +```xml +<param key="status">someValue</param> +``` + +<!-- LINK DEFINITIONS --> + +[actions]: test/actions.md +[api reference]: https://devdocs.magento.com/guides/v2.3/get-started/bk-get-started-api.html +[application/x-www-form-urlencoded]: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 +{:target="_blank"} +[catalogCategoryRepositoryV1 image]: img/catalogCategoryRepository-operations.png +[catalogCategoryRepositoryV1SavePostBody]: #catalogCategoryRepositoryV1SavePostBody +[contentType]: #contentType-tag +[Create a new category]: #create-object-as-adminOauth +[createData]: test/actions.md#createdata +[delete a category by its ID]: #delete-object-as-adminOauth +[deleteData]: test/actions.md#deletedata +[entity]: data.md#entity-tag +[get information about a category by its ID]: #get-object-as-adminOauth +[getData]: test/actions.md#getdata +[HTML forms]: https://www.w3.org/TR/html401/interact/forms.html +{:target="\_blank"} +[oauth]: https://devdocs.magento.com/guides/v2.3/get-started/authentication/gs-authentication-oauth.html +{:target="\_blank"} +[operation]: #operation-tag +[reference]: #reference +[rest request]: #handling-with-api +[html form]: #using-html-forms +[update category data by its ID]: #update-object-as-adminOauth +[updateData]: test/actions.md#updatedata +[rest response]: #rest-response + +*[CRUD]: Create Read Update Delete +*[MFTF]: Magento Functional Testing Framework diff --git a/docs/page.md b/docs/page.md new file mode 100644 index 000000000..52107e525 --- /dev/null +++ b/docs/page.md @@ -0,0 +1,172 @@ +# Page structure + +The MFTF uses a modified concept of [PageObjects], which models the testing areas of your page as objects within the code. +This reduces occurrences of duplicated code and enables you to fix things quickly, in one place, when things change. +You define the contents of a page, for reference in a [`<test>`], at both the [`<page>`] and [`<section>`] level. + +The `pageObject` lists the URL of the `page` and the `sections` that it contains. +You can reuse `sections` to define the elements on a page that are exercisable, while also ensuring a single source of truth to help maintain consistency. + +Avoid hard-coded location selectors from tests to increase the maintainability and readability of the tests, as well as improve the test execution output and logging. + +Two types of pages are available: + +<!-- {% raw %} --> + +- Page with `url` declared as a constant string or [explicit page] - In a test it is called in a format like `{{NameOfPage.url}}`, where `NameOfPage` is a value of `name` in the corresponding page declaration `*.xml` file. +- Page with `url` declared as a string with one or more variables or [parameterized page] +- In a test it is called using a format like `{{NameOfPage.url(var1, var2, ...)}}`, where `var1, var2` etc. are parameters that will be substituted in the `url` of the corresponding `<page>` declaration. + +The following diagram shows the XML structure of an MFTF page: + +![XML Structure of MFTF Page](img/page-dia.svg) + +## Format + +The format of `<page>` is: + +```xml +<?xml version="1.0" encoding="UTF-8"?> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="" url="" module="" area=""> + <section name=""/> + <section name=""/> + </page> +</pages> +``` + +## Principles + +The following conventions apply to MFTF pages: + +- `<page>` name is the same as the file name. +- `<page>` name must be alphanumeric. +- `*Page.xml` is stored in the _Page_ directory of a module. +- The name format is `{Admin|Storefront}{PageDescription}Page.xml`. + +The `.url` attribute is required when using the page for [actions] that require the URL argument. + +## Page examples + +These examples demonstrate explicit and parameterized pages and include informative explanations. + +### Explicit page + +Example (_Catalog/Page/AdminCategoryPage.xml_ file): + +```xml +<?xml version="1.0" encoding="UTF-8"?> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../..dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="AdminCategoryPage" url="catalog/category/" module="Magento_Catalog" area="admin"> + <section name="AdminCategorySidebarActionSection"/> + <section name="AdminCategorySidebarTreeSection"/> + <section name="AdminCategoryBasicFieldSection"/> + <section name="AdminCategorySEOSection"/> + </page> +</pages> +``` + +In this example, the _Catalog/Page/AdminCategoryPage.xml_ file declares a page with the name `AdminCategoryPage`. +It will be merged with the other `AdminCategoryPage` pages from other modules. + +The corresponding web page is generated by the Magento Catalog module and is called by the `baseUrl` + `backendName` + `catalog/category/` URl. + +The `AdminCategoryPage` declares four [sections][section]: + +- `AdminCategorySidebarActionSection` - located in the `Catalog/Section/AdminCategorySidebarActionSection.xml` file +- `AdminCategorySidebarTreeSection` - located in the `Catalog/Section/AdminCategorySidebarTreeSection.xml` file +- `AdminCategoryBasicFieldSection` - located in the `Catalog/Section/AdminCategoryBasicFieldSection.xml` file +- `AdminCategorySEOSection` - located in the `Catalog/Section/AdminCategorySEOSection.xml` file + +The following is an example of a call in test: + +```xml +<amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToAdminCategory"/> +``` + +### Parameterized page + +Example (_Catalog/Page/StorefrontCategoryPage.xml_ file): + +```xml +<?xml version="1.0" encoding="UTF-8"?> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../..dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="StorefrontCategoryPage" url="/{{var1}}.html" module="Magento_Catalog" parameterized="true" area="storefront"> + <section name="StorefrontCategoryMainSection"/> + </page> +</pages> +``` + +This example shows the page with the name `StorefrontCategoryPage`. +It will be merged with the other `StorefrontCategoryPage` pages from other modules. + +The following is an example of a call in test: + +```xml +<amOnPage url="{{StorefrontCategoryPage.url($$createPreReqCategory.name$$)}}" stepKey="navigateToCategoryPage"/> +``` + +The `StorefrontCategoryPage` page is declared as parameterized, where the `url` contains a `{{var1}}` parameter. + +The corresponding web page is generated by the Magento Catalog module and is called by the `baseUrl`+`/$$createPreReqCategory.name$$.html` URl. + +`{{var1}}` is substituted with the `name` of the previously created category in the `createPreReqCategory` action. + +See also [`<createData>`]. + +**** + +The `StorefrontCategoryPage` page declares only the `StorefrontCategoryMainSection` [section], which is located in the `Catalog/Section/StorefrontCategoryMainSection.xml` file. + +## Elements reference + +There are several XML elements that are used in `<page>` in the MFTF. + +### pages {#pages-tag} + +`<pages>` are elements that point to the corresponding XML Schema location. +It contains one or more `<page>` elements. + +### page {#page-tag} + +`<page>` contains a sequence of UI sections in a page. + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|required|Unique page name identifier. +`url`|string|required|URL path (excluding the base URL) for the page. Use parameterized notation (`{{var1}}`) for replaceable parameters, such as the edit page for a persisted entity that is based on an ID or a name. +`module`|string|required|Name of the module to which the page belongs. The name must be prefixed with a vendor name. It corresponds to the parent directory where the module with tests is stored. Example: `"Magento_Catalog"`. +`area`|string|required|The area where this page lives. Three possible values: `admin` prepends `BACKEND_NAME` to `url`, `storefront` does not prepend anything to `url`, `external` flags the page for use with `amOnUrl`. The `url` provided must be a full URL, such as `http://myFullUrl.com/`, instead of the URL for a Magento page. +`parameterized`|boolean |optional|Include and set to `"true"` if the `url` for this page has parameters that need to be replaced for proper use. +`remove`|boolean|optional|The default value is `"false"`. Set to `"true"` to remove this element during parsing. + +`<page>` may contain several [`<section>`] elements. + +<!-- {% endraw %} --> + +### section {#section-tag} + +`<section>` contains the sequence of UI elements. +A section is a reusable piece or part of a page. + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|required|Unique section name identifier. +`remove`|boolean|optional|The default value is `"false"`. Set to `"true"` to remove this element during parsing. + +<!-- Link definitions --> +[`<createData>`]: test/actions.md#createdata +[`<page>`]: #page-tag +[`<section>`]: #section-tag +[`<test>`]: test.md +[actions]: test/actions.md +[explicit page]: #explicit-page +[PageObjects]: https://github.com/SeleniumHQ/selenium/wiki/PageObjects +[parameterized page]: #parameterized-page +[section]: section.md diff --git a/docs/reporting.md b/docs/reporting.md new file mode 100644 index 000000000..f303945af --- /dev/null +++ b/docs/reporting.md @@ -0,0 +1,356 @@ +# Reporting + +The Magento Functional Testing Framework provides two types of reporting: + +- Inline reporting that you can view in the terminal as you run [`mftf`][mftf] or [`codecept`][codecept] CLI commands. +- HTML reports that you can view using the [Allure Framework][] after a test run completes. + +When you run a test, MFTF copies all reporting artifacts to the `dev/tests/acceptance/tests/_output` subdirectory in the Magento root directory. +The directory contains: + +- `allure-results/` that is a directory generated and served by the Allure Framework. +- `failed` that is a text file containing relative paths to failed tests after the last test run. + The paths are relative to `dev/tests/acceptance/`. +- `.html` and `.png` files that are screenshots of fails in HTML and PNG formats. + To cleanup the `_output/` directory, remove them manually. + +The `mftf` tool logs output continuously to the `dev/tests/acceptance/mftf.log` file. + +## Command line + +The MFTF reports about its progress during test run when you run the `mftf` CLI tool with [`run:test`][] or [`run:group`][] commands. + +The report can contain three main parts: + +- Pre-run checks: + - Environment check, such as PHP warnings, etc. + - XML test validation like deprecation warnings such as missing required components in XML tests. +- Codeception report which is the progress report for each test. +- Total results of the test run such as number of tests, assertions, and failures. + +To manage the level of verbosity, use `-v` or `--verbose` flag in the `mftf` commands. +To enable verbosity using the `codecept` commands, refer to the Codeception [Console Commands][codeception]. + +The following sections demonstrate an example interpretation of a complete log separated into chunks. + +### Pre-run check report + +First, the MFTF reports about issues with environment. +In our case, there is an issue with PHP library loading. + +```terminal +PHP Warning: PHP Startup: Unable to load dynamic library '/usr/local/lib/php/pecl/20160303/php_intl.dll' - dlopen(/usr/local/lib/php/pecl/20160303/php_intl.dll, 9): image not found in Unknown on line 0 +``` + +Next, the MFTF returns `DEPRECATION` reports alerting you that required test components are missing in XML test definitions. + +```terminal +DEPRECATION: Test AdminFilteringCategoryProductsUsingScopeSelectorTest is missing required annotations.{"testName":"AdminFilteringCategoryProductsUsingScopeSelectorTest","missingAnnotations":"stories"} +DEPRECATION: Test AdminAbleToShipPartiallyInvoicedItemsTest is missing required annotations.{"testName":"AdminAbleToShipPartiallyInvoicedItemsTest","missingAnnotations":"stories"} +DEPRECATION: Test AdminRemoveProductWeeeAttributeOptionTest is missing required annotations.{"testName":"AdminRemoveProductWeeeAttributeOptionTest","missingAnnotations":"stories"} +Generate Tests Command Run +``` + +`Generate Tests Command Run` indicates the moment when the MFTF has run the tests generation command actually. + +### Test execution report + +A test execution report is generated by Codeception and includes configuration information, scenario execution steps, and PASSED/FAIL verdict after each test completion. + +#### General information + +The general information can be useful for MFTF contributors, but can be ignored by a test writer. + +Let's consider the general part of the following test execution report: + +```terminal +==== Redirecting to Composer-installed version in vendor/codeception ==== +Codeception PHP Testing Framework v2.3.9 +Powered by PHPUnit 6.5.13 by Sebastian Bergmann and contributors. + +Magento\FunctionalTestingFramework.functional Tests (2) ------------------------ +Modules: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver, \Magento\FunctionalTestingFramework\Helper\Acceptance, \Magento\FunctionalTestingFramework\Helper\MagentoFakerData, \Magento\FunctionalTestingFramework\Module\MagentoRestDriver, PhpBrowser, \Magento\FunctionalTestingFramework\Module\MagentoSequence, \Magento\FunctionalTes +``` + +After the test generation command (mentioned in the previous section), the MFTF delegates control to the `vendor/codeception` tool, which is the `Codeception PHP Testing Framework` of version `2.3.9` that uses `PHPUnit` of version `6.5.13`. + +The tool runs `2 Tests` using the configuration defined in the `functional` suite under the `Magento\FunctionalTestingFramework` namespace. +The corresponding configuration file is `acceptance/tests/functional.suite.yml`. +It enables `Modules: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver, \Magento\FunctionalTestingFramework\Helper\Acceptance, \Magento\FunctionalTestingFramework\Helper\MagentoFakerData, \Magento\FunctionalTestingFramework\Module\MagentoRestDriver, PhpBrowser, \Magento\FunctionalTestingFramework\Module\MagentoSequence, ...` + +#### Passed tests + +The next chunk of the log reports about test execution of the first test: + +```terminal +AdminLoginTestCest: Admin login test +Signature: Magento\AcceptanceTest\_default\Backend\AdminLoginTestCest:AdminLoginTest +Test: tests/functional/Magento/FunctionalTest/_generated/default/AdminLoginTestCest.php:AdminLoginTest +Scenario -- +I am on page "/admin/admin" +I fill field "#username","admin" +I fill field "#login","123123q" +I click ".actions .action-primary" +I wait for page load 30 +I close admin notification +I see in current url "/admin/admin" +PASSED +``` + +The running test is `AdminLoginTestCest`, which is `Admin login test` (this text is generated from the test name but with the `Cest` part excluded). +Its test signature is `Magento\AcceptanceTest\_default\Backend\AdminLoginTestCest:AdminLoginTest` that matches a `className:methodName` format using namespaces. +A path to the corresponding `Test` is `tests/functional/Magento/FunctionalTest/_generated/default/AdminLoginTestCest.php:AdminLoginTest` (relative to the `acceptance/` directory). + +`Scenario` lists the tests steps as they run during test execution, ending with the successful test verdict `PASSED`. +It means that all test steps were processed as expected. + +#### Failed tests + +The second test fails with the following report: + +```terminal +AdminMenuNavigationWithSecretKeysTestCest: Admin menu navigation with secret keys test +Signature: Magento\AcceptanceTest\_default\Backend\AdminMenuNavigationWithSecretKeysTestCest:AdminMenuNavigationWithSecretKeysTest +Test: tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:AdminMenuNavigationWithSecretKeysTest +Scenario -- +I magento cli "config:set admin/security/use_form_key 1" +Value was saved. +I magento cli "cache:clean config full_page" +Cleaned cache types: +config +full_page +I am on page "/admin/admin" +I wait for page load +I fill field "#username","admin" +I fill field "#login","123123q" +I click ".actions .action-primary" +I wait for page load 30 +I close admin notification +I click "//li[@id='menu-magento-backend-stores']" +I wait for loading mask to disappear +I click "#nav li[data-ui-id='menu-magento-config-system-config']" +I wait for page load +I see current url matches "~\/admin\/system_config\/~" +I see "#something" +I save screenshot +FAIL + +I magento cli "config:set admin/security/use_form_key 0" +Value was saved. +I magento cli "cache:clean config full_page" +Cleaned cache types: +config +full_page +I am on page "/admin/admin/auth/logout/" +-------------------------------------------------------------------------------- +``` + +The general test details and scenario has the same format as in the Passed test. +The interesting part starts near the `FAIL` line. + +```terminal +I see "#something" +I save screenshot +FAIL +``` + +When a test step fails, the MFTF always saves a screenshot of the web page with the failing state immediately after the failure occurs. +`I save screenshot` follows the failing test step `I see "#something"` in our case. + +A screenshot of the fail goes at the `acceptance/tests/_output` directory in both PNG and HTML formats: + +- `Magento.AcceptanceTest._default.Backend.AdminMenuNavigationWithSecretKeysTestCest.AdminMenuNavigationWithSecretKeysTest.fail.html` +- `Magento.AcceptanceTest._default.Backend.AdminMenuNavigationWithSecretKeysTestCest.AdminMenuNavigationWithSecretKeysTest.fail.png` + +The file name encodes: + +- `Magento` namespace +- with the `AcceptanceTest` test type +- generated as a part of the `_default` suite +- defined at the `Magento_Backend` module +- implemented in the `AdminMenuNavigationWithSecretKeysTestCest` PHP class +- with the `AdminMenuNavigationWithSecretKeysTest` test name +- and execution status `fail` + +Actions after `FAIL` are run as a part of the [`after`][] hook of the test. + +### Test result report + +After the MFTF completed test execution, it generates a general report about test results along with detailed information about each fail. + +```terminal +-------------------------------------------------------------------------------- +DEPRECATION: Calling the "Symfony\Component\BrowserKit\Client::getInternalResponse()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0. /Users/.../magento2ce/vendor/symfony/browser-kit/Client.php:208 + +Time: 52.43 seconds, Memory: 16.00MB + +There was 1 failure: +--------- +``` + +First you see warnings and deprecations. +The `DEPRECATION` here is thrown by an MFTF dependency (Symfony) that is out of the scope for test writers and should be considered by MFTF contributors. +If you encounter this type of reporting, [report an issue][]. + +Then, the MFTF reports that the test run took 52.43 seconds using 16 MB of system RAM. +And, finally, that there was `1 failure`. + +Next, the report provides details about the test failure. + +```terminal +--------- +1) AdminMenuNavigationWithSecretKeysTestCest: Admin menu navigation with secret keys test +Test tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:AdminMenuNavigationWithSecretKeysTest +Step See "#something" +Fail Failed asserting that on page /admin/admin/system_config/index/key/678b7ba922c.../ +--> DASHBOARD +SALES +CATALOG +CUSTOMERS +MARKETING +CONTENT +REPORTS +STORES +SYSTEM +FIND PARTNERS & EXTENSIONS +Configuration +admin +1 +Store View: Default Config +What is this? +Save Config +Country Options +State Options +Locale Options +Store Information +Store Name +Store Phone Number +Store Hours of Operation +Countr +[Content too long to display. See complete response in '/Users/dmytroshevtsov/Projects/vagrant/vagrant-magento/magento2ce/dev/tests/acceptance/tests/_output/' directory] +--> contains "#something". + +Scenario Steps: + +23. $I->amOnPage("/admin/admin/auth/logout/") at tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:54 +22. // Cleaned cache types: +config +full_page +21. $I->magentoCLI("cache:clean config full_page") at tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:52 +20. // Value was saved. +19. $I->magentoCLI("config:set admin/security/use_form_key 0") at tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:50 +18. $I->saveScreenshot() at tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:63 +``` + +- `1) AdminMenuNavigationWithSecretKeysTestCest: Admin menu navigation with secret keys test` - the failed Codeception test is *AdminMenuNavigationWithSecretKeysTestCest*. It references to the PHP class that implemented the failed test. + +- `Test tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:AdminMenuNavigationWithSecretKeysTest` - the test is implemented in the *AdminMenuNavigationWithSecretKeysTest* test method of the *tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php* file under `<magento root>/dev/tests/acceptance/`. + It matches the corresponding test defined in XML that is *AdminMenuNavigationWithSecretKeysTest* defined in `<test name="AdminMenuNavigationWithSecretKeysTest">...</test>` + +- `Step See "#something"` - the failing test step is the *see* action with the *#something* selector. It would correspond the `<see selector="#something" ... />` test step in the XML defined tests. + +- `Fail Failed asserting that on page /admin/admin/system_config/index/key/678b7ba922c.../` - the fail occurred on the web page `<MAGENTO_BASE_URL>/admin/admin/system_config/index/key/678b7ba922c.../`. + +```terminal +--> ... +[Content too long to display. See complete response in '/../../magento2/dev/tests/acceptance/tests/_output/' directory] +--> contains "#something". +``` + +The web page is too long to be reported in the CLI, and it is stored at *'/../../magento2/dev/tests/acceptance/tests/_output/'*. +Search the web page by test name *AdminMenuNavigationWithSecretKeysTest*. +The failing test assertion is that the web page contains *contains* a CSS locator *#something*. + +Finally, the report finishes with fairly self-descriptive lines. + +```terminal +FAILURES! +Tests: 2, Assertions: 3, Failures: 1. +``` + +The MFTF encountered failures due to the last test run, that included *2* tests with *3* assertions. +*1* assertion fails. + +## Allure + +Each time you run tests, the MFTF appends an XML file with results at the `tests/_output/allure-results/` directory. + +The official [Allure Test Report][] documentation is well-covered, so we'll list only the CLI commands that you would need for your day-to-day work. + +<div class="bs-callout bs-callout-info"> +The following commands are relative to the Magento installation directory. +</div> + +To generate a report to the `allure-report/` at the current directory: + +```bash +allure generate dev/tests/acceptance/tests/_output/allure-result +``` + +To generate a report to a particular directory, use the `-o` option: + +```bash +allure generate dev/tests/acceptance/tests/_output/allure-result -o dev/tests/acceptance/tests/_output/allure-report +``` + +To launch the generated report in a web browser: + +```bash +allure open dev/tests/acceptance/tests/_output/allure-report +``` + +<div class="bs-callout bs-callout-info" markdown="1"> +By default, Allure generates reports in the `allure-report/` at the current directory. +For example, if you run the command without `-o` flag while you are in the `magento2/` directory, Allure will generate a report at the `magento2/allure-report/` directory. +</div> + +```bash +allure generate dev/tests/acceptance/tests/_output/allure-result +``` + +Example of file structure after the command run: + +```terminal +magento2 +├── allure-report +├── app +├── bin +├── dev +├── ... +``` + +And if you run the `open` command with no arguments while you are in the same directory (`magento2/`): + +```bash +allure open +``` + +Allure would attempt to open a generated report at the `magento2/allure-report/` directory.' +%} + +To clean up existing reports before generation (for example after getting new results), use the `--clean` flag: + +```bash +allure generate dev/tests/acceptance/tests/_output/allure-result --clean +``` + +To generate the HTML Allure report in a temporary folder and open the report in your default web browser: + +```bash +allure serve dev/tests/acceptance/tests/_output/allure-results/ +``` + +Refer to the [Reporting section][] for more Allure CLI details. + +<!-- Link definitions --> + +[`after`]: test.md#after-tag +[`run:group`]: commands/mftf.md#rungroup +[`run:test`]: commands/mftf.md#runtest +[Allure Framework]: https://docs.qameta.io/allure/ +[Allure Test Report]: http://allure.qatools.ru/ +[codecept]: commands/codeception.md +[codeception]: https://codeception.com/docs/reference/Commands +[mftf]: commands/mftf.md +[report an issue]: https://github.com/magento/magento2-functional-testing-framework/blob/master/.github/CONTRIBUTING.md#report-an-issue +[Reporting section]: https://docs.qameta.io/allure/#_reporting diff --git a/docs/section.md b/docs/section.md new file mode 100644 index 000000000..745d2e574 --- /dev/null +++ b/docs/section.md @@ -0,0 +1,152 @@ +# Section structure + +A `<section>` is a reusable part of a [`<page>`](./page.html) and is the standard file for defining UI elements on a page used in a test. + +A `<section>` can define: + +<!-- {% raw %} --> + +- An explicit element that has a selector equal to the constant string. Example: `selector="#add_root_category_button"` +- A parameterized element that contains substitutable values in the selector. Example: `selector="#element .{{var1}} .{{var2}}"`. + +## Substitutable values + +Substitutable values in the test can be of the following formats: + +- String literals (`stringLiteral`) +- References to a [data entity][] (XML data from the corresponding `.../Data/*.xml`) such as `entityName.Field`. +- Persisted data: + - `$persistedCreateDataKey.field$` for data created in the scope of a [test][] using the [`<createData>`][] action with `stepKey="persistedCreateDataKey"`. + - `$$persistedCreateDataKey.field$$` for data created in [before][] and [after][] hooks. Even though `<before>`and `<after>` are nested inside a [test][], persisted data is stored differently when it is done in a test hook. Therefore it must be accessed with a different notation. + +The following diagram shows the XML structure of an MFTF section: + +![XML Structure of MFTF section](img/section-dia.svg) + +## Format + +The format of a `<section>` is: + +```xml +<?xml version="1.0" encoding="UTF-8"?> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name=""> + <element name="" type="" selector="" /> + <element name="" type="" selector="" parameterized="true"/> + <element name="" type="" selector="" timeout=""/> + </section> +</sections> +``` + +## Principles + +The following conventions apply to MFTF sections: + +- `<section>` name must be alphanumeric. +- `*Section.xml` is stored in the _Section_ directory of a module. +- The name format is `{Admin|Storefront}{SectionDescription}Section.xml`. +- Camel case is used for `<section>` elements. + They describe the function of the element rather than attempting to describe the selector used. + +## Example + +Example (`.../Catalog/Section/AdminCategorySidebarActionSection.xml` file): + +```xml +<?xml version="1.0" encoding="utf-8"?> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCategorySidebarActionSection"> + <element name="addRootCategoryButton" type="button" selector="#add_root_category_button" timeout="30"/> + <element name="addSubcategoryButton" type="button" selector="#add_subcategory_button" timeout="30"/> + </section> +</sections> +``` + +This example uses a `AdminCategorySidebarActionSection` section. All sections with same name will be merged during test generation. + +The `AdminCategorySidebarActionSection` section declares two buttons: + +- `addRootCategoryButton` - button with a `#add_root_category_button` locator on the parent web page +- `addSubcategoryButton` - button with a `#add_subcategory_button` locator on the parent web page + +The following is an example of a call in test: + +```xml +<!-- Click on the button with locator "#add_subcategory_button" on the web page--> +<click selector="{{AdminCategorySidebarActionSection.addSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> +``` + +## Elements reference + +### section {#section-tag} + +`<section>` contains the sequence of UI elements in a section of a [page][]. + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|required|Unique section name identifier. +`remove`|boolean|optional|The default is `false`. Set to `true` to remove this element during parsing. + +### element {#element-tag} + +`<element>`is a UI element used in an [action][]. + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|required|The element name; Must be alphanumeric. +`type`|string|required|The type of the element. Possible values: `text`, `textarea`, `input`, `button`, `checkbox`, `radio`, `checkboxset`, `radioset`, `date`, `file`, `select`, `multiselect`, `wysiwyg`, `iframe`, `block`. +`selector`|string|optional|[XPath][] or [CSS][] selector of the element. +`locatorFunction`|string|optional|[Locator function][] declaration to be used in lieu of a selector. +`timeout`|string|optional|The timeout after interaction with the element (in seconds). The default is _none_. +`parameterized`|boolean|optional|Include and set to `true` if the `selector` for this element has parameters that need to be replaced for proper use. Learn more in [Parameterized selectors][]. +`remove`|boolean|optional|The default is `false`. Set to `true` to remove this element during parsing. + +#### `timeout` attribute {#timeout-attribute} + +The attribute adds the [waitForPageLoad] action after a reference to the element in test. +The most usual use case is a test step with a button click action. + +**Use case**: Set a timeout of 30 seconds after clicking the `signIn` button. + +The section element code declaration containing the timeout attribute: + +> StorefrontSigninSection.xml + +```xml +... +<element name="signIn" type="button" selector="#signIn" timeout="30"/> +... +``` + +The test step that covers the use case: + +> StorefrontSigninTest.xml + +```xml +... +<click selector="{{StorefrontSigninSection.signIn}}" ../> +... +``` + +<!-- {% endraw %} --> + +Whenever the `signIn` button is used in a test, the MFTF will add a 30 second `waitForPageLoad` action immediately after the `click`. + +<!-- Link definitions --> + +[waitForPageLoad]: test/actions.html#waitforpageload +[data entity]: ./data.html +[test]: ./test.html#test-tag +[`<createData>`]: ./test/actions.html#createdata +[before]: ./test.html#before-tag +[after]: ./test.html#after-tag +[page]: ./page.html +[action]: ./test/actions.html +[XPath]: https://www.w3schools.com/xml/xpath_nodes.asp +[CSS]: https://www.w3schools.com/cssref/css_selectors.asp +[Locator function]: ./section/locator-functions.html +[Parameterized selectors]: ./section/parameterized-selectors.html \ No newline at end of file diff --git a/docs/section/locator-functions.md b/docs/section/locator-functions.md new file mode 100644 index 000000000..d52c2fc72 --- /dev/null +++ b/docs/section/locator-functions.md @@ -0,0 +1,46 @@ +# Locator functions + + +## Define Locator::functions in elements + + Codeception has a set of very useful [Locator functions][] that may be used by elements inside a [section][]. + +Declare an element with a `locatorFunction`: + +```xml +<element name="simpleLocator" type="button" locatorFunction="Locator::contains('label', 'Name')"/> +``` + +When using the `locatorFunction`, omit `Locator::` for code simplicity: + +```xml +<element name="simpleLocatorShorthand" type="button" locatorFunction="contains('label', 'Name')"/> +``` + +An element's `locatorFunction` can also be parameterized the same way as [parameterized selectors][]: + +<!-- {% raw %} --> + +```xml +<element name="simpleLocatorTwoParam" type="button" locatorFunction="contains({{arg1}}, {{arg2}})" parameterized="true"/> +``` + +An element cannot, however, have both a `selector` and a `locatorFunction`. + +## Call Elements that use locatorFunction + +Given the above element definitions, you call the elements in a test just like any other element. No special reference is required, as you are still just referring to an `element` inside a `section`: + +```xml +<test name="LocatorFuctionTest"> + <click selector="{{LocatorFunctionSection.simpleLocator}}" stepKey="SimpleLocator"/> + <click selector="{{LocatorFunctionSection.simpleLocatorTwoParam('string1', 'string2')}}" stepKey="TwoParamLiteral"/> +</test> +``` + +<!-- {% endraw %} --> + +<!-- Link Definitions --> +[Locator functions]: http://codeception.com/docs/reference/Locator +[section]: ../section.md +[parameterized selectors]: ./parameterized-selectors.md \ No newline at end of file diff --git a/docs/section/parameterized-selectors.md b/docs/section/parameterized-selectors.md new file mode 100644 index 000000000..58227948f --- /dev/null +++ b/docs/section/parameterized-selectors.md @@ -0,0 +1,155 @@ +# Parameterized selectors + +Use the following examples to create and use parameterized selectors in the MFTF. + +## Set up a selector in section + +Create a new `<element/>` in a `<section></section>`, : + +```xml +<section name="SampleSection"> + <element name="" type="" selector=""/> +</section> +``` + +Add the attribute `parameterized="true"` to the `<element/>`: + +```xml +<section name="SampleSection"> + <element name="" type="" selector="" parameterized="true"/> +</section> +``` + +Add your selector in the `selector=""` attribute: + +```xml +<section name="SampleSection"> + <element name="" type="" selector="#element" parameterized="true"/> +</section> +``` + +<!-- {% raw %} --> + +### Selector with single variable + +For the parameterized part of the selector, add `{{var1}}` to represent the first piece of data that you want to replace: + +```xml +<section name="SampleSection"> + <element name="" type="" selector="#element .{{var1}}" parameterized="true"/> +</section> +``` + +Add a descriptive name in the `name=""` attribute: + +```xml +<section name="SampleSection"> + <element name="oneParamElement" type="" selector="#element .{{var1}}" parameterized="true"/> +</section> +``` + +Add the type of UI element that the `<element/>` represents in `type`: + +```xml +<section name="SampleSection"> + <element name="oneParamElement" type="text" selector="#element .{{var1}}" parameterized="true"/> +</section> +``` + +### Selector with multiple variables + +For the parameterized part of the selector, add `{{var1}}, {{var2}}, ..., {{varN}}` for each parameter that you need to pass in: + +```xml +<section name="SampleSection"> + <element name="threeParamElement" type="text" selector="#element .{{var1}} .{{var2}}" parameterized="true"/> +</section> +``` + +```xml +<section name="SampleSection"> + <element name="threeParamElement" type="text" selector="#element .{{var1}} .{{var2}}-{{var3}}" parameterized="true"/> +</section> +``` + +<div class="bs-callout bs-callout-info" markdown="1"> +There is no need to use sequential variables like `{{var1}}`, `{{var2}}`. Parameterized replacement reads variables and maps them to the test call of the element sequentially from left to right, meaning you can use a selector like `#element .{{categoryId}} .{{productId}}`." +</div> + +## Use a parameterized selector in a test + +Create a new [test][]: + +```xml +<test name="SampleTest"> + +</test> +``` + +Add an action: + +```xml +<test name="SampleTest"> + <click selector="" stepKey="click1"/> +</test> +``` + +Enter `"{{}}"` in the `selector=""` attribute: + +```xml +<test name="SampleTest"> + <click selector="{{}}" stepKey="click1"/> +</test> +``` + +Make a reference to the section that the element is assigned to inside the `{{}}`: + +```xml +<test name="SampleTest"> + <click selector="{{SampleSection}}" stepKey="click1"/> +</test> +``` + +Add name of a parameterized element, separated by `"."`, inside the `{{}}`: + +```xml +<test name="SampleTest"> + <click selector="{{SampleSection.threeParamElement}}" stepKey="click1"/> +</test> +``` + +Add a set of `"()"` following the parameterized element inside the `{{}}`: + +```xml +<test name="SampleTest"> + <click selector="{{SampleSection.threeParamElement()}}" stepKey="click1"/> +</test> +``` + +Add the first parameter, that you would like to pass to the selector, inside of the `()`: + +```xml +<test name="SampleTest"> + <click selector="{{SampleSection.threeParamElement(_defaultCategory.is_active)}}" stepKey="click1"/> +</test> +``` + +Add the second or third parameters, that you'd like to pass to the selector, separated by `, `: + +```xml +<test name="SampleTest"> + <click selector="{{SampleSection.threeParamElement(_defaultCategory.is_active,'StringLiteral',$createDataKey.id$)}}" stepKey="click1"/> +</test> +``` + +<!-- {% endraw %} --> + +Any data can be used in parameterized elements, as well as entered in test actions: + +* `_defaultCategory.is_active` is a reference to `<data key="is_active">` in `<entity name="_defaultCategory" ... ></entity>` in the corresponding `.../Data/*.xml`. +* `'StringLiteral'` is a literal. +* `$createDataKey.id$` is a reference to persisted data created in the `SampleTest1` within the `stepKey="createDataKey"` action. +* `{$variable}` is a reference to data returned by a test action, like `<grabValueFrom>`. + +<!-- Link Definitions --> +[test]: ../test.md \ No newline at end of file diff --git a/docs/suite.md b/docs/suite.md new file mode 100644 index 000000000..b24b3b9bf --- /dev/null +++ b/docs/suite.md @@ -0,0 +1,294 @@ +# Suites + +Suites are essentially groups of tests that run in the specific conditions (preconditions and postconditions). +They enable you including, excluding, and grouping tests for a customized test run when you need it. +You can form suites using separate tests, groups, and modules. + +Each suite must be defined in the `<magento 2 root>/dev/tests/acceptance/tests/_suite/suite.xml` file. +The generated tests for each suite go into a separate directory under `<magento 2 root>/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/_generated/`. +By default, all generated tests are stored in the _default_ suite under `.../Magento/FunctionalTest/_generated/default/` + +<div class="bs-callout bs-callout-info"> + If a test is generated into at least one custom suite, it will not appear in the _default_ suite. +</div> + +## Format + +The format of a suite: + +```xml +<?xml version="1.0" encoding="UTF-8"?> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> + <suite name=""> + <before> + </before> + <after> + </after> + <include> + <test name=""/> + <group name=""/> + <module name="" file=""/> + </include> + <exclude> + <test name=""/> + <group name=""/> + <module name="" file=""/> + </exclude> + </suite> +</suites> +``` + +## Principles + +- A suite name: + - must not match any existing group value. + For example, the suite `<suite name="ExampleTest">` will fail during test run if any test contains in annotations `<group value="ExampleTest">`. + - must not be `default` or `skip`. Tests that are not in any suite are generated under the `default` suite. + The suite name `skip` is synonymous to including a test in the `<group value="skip"/>`, which will be deprecated in MFTF 3.0.0. + - can contain letters, numbers, and underscores. + - should be upper camel case. +- A suite must contain at least one `<include>`, or one `<exclude>`, or both. +- Using `<before>` in a suite, you must add the corresponding `<after>` to restore the initial state of your testing instance. + +## Conditions + +Using suites enables test writers to consolidate conditions that are shared between tests. +The code lives in one place and executes once per suite. + +- Set up preconditions and postconditions using [actions] in [`<before>`] and [`<after>`] correspondingly, just similar to use in a [test]. +- Clean up after suites just like after tests. + The MFTF enforces the presence of both `<before>` and `<after>` if either is present. +- Do not reference in the subsequent tests to data that was persisted in the preconditions. + Referencing to `$stepKey.field$` of these actions is not valid. + +## Test writing + +Since suites enable precondition consolidation, a common workflow for test writing is adding a new test to an existing suite. +Such test is generated in context of the suite that contains it. +You cannot isolate this test from preconditions of the suite; it cannot be used outside of the suite at the same time. + +There are several ways to generate and execute your new test in the context of a suite: + +- Edit the appropriate `suite.xml` to include your test only and run: + + ```bash + vendor/bin/mftf run:group <suiteName> + ``` + +- Temporarily add a group to your test like `<group value="foo">` and run: + + ```bash + vendor/bin/mftf run:group foo + ``` + +- To limit generation to your suite/test combination, run in conjunction with the above: + + ```bash + vendor/bin/mftf generate:suite <suite> + ``` + +- To generate any combination of suites and tests, use [`generate:tests`] with the `--tests` flag. + +## Examples + +### Enabling/disabling WYSIWYG in suite conditions + +<!-- {% raw %} --> + +```xml +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> + <suite name="WYSIWYG"> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <amOnPage url="admin/admin/system_config/edit/section/cms/" stepKey="navigateToConfigurationPage" /> + <waitForPageLoad stepKey="wait1"/> + <conditionalClick stepKey="expandWYSIWYGOptions" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> + <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitForEnableWYSIWYGDropdown1" /> + <waitForElementVisible selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="waitForUseSystemValueVisible"/> + <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Enabled by Default" stepKey="selectOption1"/> + <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> + </before> + <after> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <actionGroup ref="DisabledWYSIWYG" stepKey="disable"/> + </after> + <include> + <group name="WYSIWYG"/> + </include> + </suite> +</suites> +``` + +<!-- {% endraw %} --> +This example declares a suite with the name `WYSIWYG`. +The suite enables WYSIWYG *before* running tests. +It performs the following steps: + +1. Log in to the backend. +2. Navigate to the **Configuration** page. +3. Enable **WYSIWYG** in the Magento instance. + +*After* the testing, the suite returns the Magento instance to the initial state disabling WYSIWYG: + +1. Log back in. +2. Disable **WYSIWYG** so that + +This suite includes all tests that contain the `<group value="WYSIWYG"/>` annotation. + +### Execute Magento CLI commands in suite conditions + +```xml +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> + <suite name="Cache"> + <before> + <magentoCLI stepKey="disableCache" command="cache:disable"/> + </before> + <after> + <magentoCLI stepKey="enableCache" command="cache:enable"/> + </after> + <include> + <test name="SomeCacheRelatedTest"/> + <group name="CacheRelated"/> + </include> + </suite> +</suites> +``` + +This example declares a suite with the name `Cache`. + +Preconditions: + +1. It disables the Magento instance cache entirely before running the included tests. +2. After the testing, it re-enables the cache. + +The suite includes a specific test `SomeCacheRelatedTest` and every `<test>` that includes the `<group value="CacheRelated"/>` annotation. + +### Change Magento configurations in suite conditions + +```xml +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> + <suite name="PaypalConfiguration"> + <before> + <createData entity="SamplePaypalConfig" stepKey="createSamplePaypalConfig"/> + </before> + <after> + <createData entity="DefaultPayPalConfig" stepKey="restoreDefaultPaypalConfig"/> + </after> + <include> + <module name="Catalog"/> + </include> + <exclude> + <test name="PaypalIncompatibleTest"/> + </exclude> + </suite> +</suites> +``` + +This example declares a suite with the name `PaypalConfiguration`: + +- `<before>` block persists a Paypal Configuration enabling all tests in this suite to run under the newly reconfigured Magento instance. +- `<after>` block deletes the persisted configuration, returning Magento to its initial state. +- The suite includes all tests from the `Catalog` module, except the `PaypalIncompatibleTest` test. + +## Elements reference + +### suites {#suites-tag} + +The root element for suites. + +### suite {#suite-tag} + +A set of "before" and "after" preconditions, and test filters to include and exclude tests in the scope of suite. + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|required|Unique suite name identifier. +`remove`|boolean|optional|Removing the suite during merging. + +It can contain `<before>`, `<after>`, `<include>`, and `<exclude>`. + +### before {#before-tag} + +A suite hook with preconditions that executes once before the suite tests. + +It may contain test steps with any [actions] and [action groups]. + +<div class="bs-callout bs-callout-info"> +Tests in the suite are not run and screenshots are not saved in case of a failure in the before hook. +To troubleshoot the failure, run the suite locally. +</div> + +### after {#after-tag} + +A suite hook with postconditions executed once after the suite tests. + +It may contain test steps with any [actions] and [action groups]. + +### include {#include-tag} + +A set of filters that you can use to specify which tests to include in the test suite. + +It may contain filters by: + +- test which names a specific `<test>`. +- group which refers to a declared `group` annotation. +- module which refers to `test` files under a specific Magento Module. + +The element can contain [`<test>`], [`<group>`], and [`<module>`]. + +### exclude {#exclude-tag} + +A set of filters that you can use to specify which tests to exclude in the test suite. + +There two types of behavior: + +1. Applying filters to the included tests when the suite contains [`<include>`] filters. + The MFTF will exclude tests from the previously included set and generate the remaining tests in the suite. +2. Applying filter to all tests when the suite does not contain [`<include>`] filters. + The MFTF will generate all existing tests except the excluded. + In this case, the custom suite will contain all generated tests except excluded, and the _default_ suite will contain the excluded tests only. + +It may contain filters by: + +- test which names a specific `<test>`. +- group which refers to a declared `group` annotation. +- module which refers to `test` files under a specific Magento Module. + +The element may contain [`<test>`], [`<group>`], and [`<module>`]. + +### test {#test-tag} + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|required|Filtering a test by its name. +`remove`|boolean|optional|Removing the filter during merging. + +### group {#group-tag} + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|required|Filtering tests by the `<group>` annotation. +`remove`|boolean|optional|Removing the filter during merging. + +### module {#module-tag} + +Attributes|Type|Use|Description +---|---|---|--- +`name`|string|required|Filtering tests by their location in the corresponding module. +`file`|string|optional|Filtering a specific test file in the module. +`remove`|boolean|optional|Removing the filter during merging. + +<!-- Link definitions --> +[actions]: test/actions.md +[action groups]: test/action-groups.md +[`<after>`]: #after-tag +[`<before>`]: #before-tag +[`generate:tests`]: commands/mftf.md#generatetests +[test]: test.md +[`<test>`]: #test-tag +[`<group>`]: #group-tag +[`<module>`]: #module-tag +[`<include>`]: #include-tag \ No newline at end of file diff --git a/docs/test.md b/docs/test.md new file mode 100644 index 000000000..462cd138e --- /dev/null +++ b/docs/test.md @@ -0,0 +1,147 @@ +# Test + +Test cases in the Magento Functional Testing Framework (MFTF) are defined in XML as [`<tests>`]. +`<tests>` is a [Codeception test container][Codeception] that contains multiple individual tests with test metadata and before and after actions. + +MFTF `<tests>` is considered a sequence of actions with associated parameters. +Any failed [assertion] within a test constitutes a failed test. + +<div class="bs-callout bs-callout-info" markdown="1"> + `<before>` and `<after>` hooks are not global within `<tests>`. +They only apply to the `<test>` in which they are declared. +The steps in `<after>` are run in both successful **and** failed test runs. +</div> + +The following diagram shows the structure of an MFTF test case: + +![Structure of MFTF test case](img/test-dia.svg) + +## Format + +The format of `<tests>` is: + +```xml +<?xml version="1.0" encoding="UTF-8"?> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="" insertBefore="" insertAfter=""> + <annotations> + <!-- TEST ANNOTATIONS --> + </annotations> + <before> + <!-- ACTIONS AND ACTION GROUPS PERFORMED BEFORE THE TEST --> + </before> + <after> + <!-- ACTIONS AND ACTION GROUPS PERFORMED AFTER THE TEST --> + </after> + <!-- TEST ACTIONS, ACTION GROUPS, AND ASSERTIONS--> + </test> +</tests> +``` + +## Principles + +The following conventions apply to MFTF tests: + +* All names within the framework are in the CamelCase format. +* `<test>` name must be alphanumeric. +* Each action and action group has its own identifier `<stepKey>` for reference purposes. +* A test may have any number of [assertions][assertion] at any point within the `<test>`. +* If `<test>` is included in `<suite>`, it **cannot be generated in isolation** to the rest of the contents of the suite (see [suites] for details). + +Multiple `<test>` tags per XML file can make it hard to find and organize tags. +To simplify, we generate one `test.php` file per `<test>` tag provided, though we support both single and multiple `<test>` tags per XML file. + +## Elements reference + +There are several XML elements that are used in `<tests>` in the MFTF. + +### tests {#tests-tag} + +`<tests>` is a container for multiple tests. It is a group of test methods that define test flows within a test case. + +`<tests>` must contain at least one [`<test>`]. + +### test {#test-tag} + +`<test>` is a set of steps, including [actions] and [assertions][assertion]. It is a sequence of test steps that define test flow within a test method. + +Attribute|Type|Use|Description +---|---|---|--- +`name`|string|optional|The test identifier. +`remove`|boolean|optional|Set `true` to remove the test when merging. +`insertBefore`|string|optional| This option is used for [merging]. It enables you to add all test actions contained in the original test into a test with the same name BEFORE the test step with `stepKey` that you assigned in `insertBefore`. +`insertAfter`|string|optional| Set `stepKey` of the test step after which you want to insert the test when [merging]. +`extends`|string|optional|A name of the parent test to [extend]. + +`<test>` may also contain [`<annotations>`], [`<before>`], [`<after>`], any [action][actions], or [`<actionGroup>`]. + +### annotations {#annotations-tag} + +[Annotations] are supported by both [Codeception] and [Allure]. + +Codeception annotations typically provide metadata and are able to influence test selection. +Allure annotations provide metadata for reporting. + +### before {#before-tag} + +`<before>` wraps the steps to perform before the [`<test>`]. + +`<before>` may contain these child elements: + +* Any [`<action>`][actions] +* [`<actionGroup>`] + +### after {#after-tag} + +`<after>` wraps the steps to perform after the [`<test>`]. +The steps are run in both successful **and** failed test runs. + +`<after>` may contain: + +* Any [`<action>`][actions] +* [`<actionGroup>`] + +### actionGroup {#actiongroup-tag} + +`<actionGroup>` calls a corresponding [action group]. + +Attribute|Type|Use|Description +---|---|---|--- +`ref`|string|required|References the required action group by its `name`. +`stepKey`|string|required| Identifies the element within `<test>`. +`before`|string|optional| `<stepKey>` of an action or action group that must be executed next while merging. +`after`|string|optional| `<stepKey>` of an action or action group that must be executed one step before the current one while merging. + +`<actionGroup>` may contain [`<argument>`]. + +### argument {#argument-tag} + +`<argument>` sets an argument that is used in the parent [`<actionGroup>`]. + +Attribute|Type|Use +---|---|--- +`name`|string|optional| Name of the argument. +`value`|string|optional| Value of the argument. + +See [Action groups][action group] for more information. + +<!-- Link definitions --> + +[`<actionGroup>`]: #actiongroup-tag +[`<after>`]: #after-tag +[`<annotations>`]: #annotations-tag +[`<argument>`]: #argument-tag +[`<before>`]: #before-tag +[`<test>`]: #test-tag +[`<tests>`]: #tests-tag +[action group]: ./test/action-groups.md +[actions]: ./test/actions.md +[Allure]: https://github.com/allure-framework/ +[Annotations]: ./test/annotations.md +[assertion]: ./test/assertions.md +[Codeception]: https://codeception.com/docs/07-AdvancedUsage +[extend]: extending.md +[merging]: ./merging.md#insert-after +[suites]: ./suite.md \ No newline at end of file diff --git a/docs/test/action-groups.md b/docs/test/action-groups.md new file mode 100644 index 000000000..0743481f6 --- /dev/null +++ b/docs/test/action-groups.md @@ -0,0 +1,264 @@ +# Action groups + +In the MFTF, you can re-use a group of [actions][], such as logging in as an administrator or a customer, declared in an XML file when you need to perform the same sequence of actions multiple times. + +The following diagram shows the structure of an MFTF action group: + +![Structure of MFTF action group](../img/action-groups-dia.svg) + +## Principles + +The following conventions apply to MFTF action groups: + +- All action groups are declared in XML files and stored in the `<module>/ActionGroup/` directory. +- Every file name ends with `ActionGroup`, such as `LoginToAdminActionGroup`. + +The XML format for the `actionGroups` declaration is: + +```xml +<?xml version="1.0" encoding="UTF-8"?> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name=""> + <arguments> + <argument name=""/> + <argument name="" defaultValue=""/> + <argument name="" defaultValue="" type=""/> + </arguments> + </actionGroup> +</actionGroups> +``` + +## Example + +These examples build a declaration for a group of actions that grant authorization to the Admin area, and use the declaration in a test. + +The _Backend/ActionGroup/LoginToAdminActionGroup.xml_ `<actionGroup>` relates to the functionality of the _Backend_ module. +In [test][], the name and identifier of the `<actionGroup>` is used as a reference in the `ref` parameter, such as `ref="LoginToAdminActionGroup"`. + +### Create an action group declaration + +To create the `<actionGroup>` declaration: + +1. Begin with a _Backend/ActionGroup/LoginToAdminActionGroup.xml_ template for the `<actionGroup>`: + + ```xml + <?xml version="1.0" encoding="UTF-8"?> + + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginToAdminActionGroup"> + ... + </actionGroup> + </actionGroups> + ``` + +<!-- {% raw %} --> + +1. Add actions to the `actionGroup` arguments: + + ```xml + <actionGroup name="LoginToAdminActionGroup"> + <fillField stepKey="fillUsername" selector="#username" userInput="{{adminUser.username}}" /> + <fillField stepKey="fillPassword" selector="#password" userInput="{{adminUser.password}}" /> + <click stepKey="click" selector="#login" /> + </actionGroup> + ``` + +1. The `userInput` variable must contain a data value for test. + Add a default data value for the variable to use in the most common cases. + For this example, the default value is `_defaultAdmin`. + + ```xml + <argument name="adminUser" defaultValue="_defaultAdmin"/> + ``` + +1. The following example shows the complete declaration: + + ```xml + <?xml version="1.0" encoding="UTF-8"?> + + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginToAdminActionGroup"> + <arguments> + <argument name="adminUser" defaultValue="_defaultAdmin"/> + </arguments> + <fillField stepKey="fillUsername" selector="#username" userInput="{{adminUser.username}}" /> + <fillField stepKey="fillPassword" selector="#password" userInput="{{adminUser.password}}" /> + <click stepKey="click" selector="#login" /> + </actionGroup> + </actionGroups> + ``` + +### Use the declaration in a test + +In this test example, we want to add the following set of actions: + +```xml +<fillField stepKey="fillUsername" selector="#username" userInput="{{CustomAdminUser.username}}" /> +<fillField stepKey="fillPassword" selector="#password" userInput="{{CustomAdminUser.password}}" /> +<click stepKey="click" selector="#login" /> +``` + +Instead of adding this set of actions, use the _LoginToAdminActionGroup_ `<actionGroup>` declaration in tests: + +1. Reference the `LoginToAdminActionGroup` action group: + + ```xml + <actionGroup stepKey="loginToAdminPanel" ref="LoginToAdminActionGroup"/> + ``` + +1. Update the argument name/value pair to `adminUser` and `CustomAdminUser`: + + ```xml + <actionGroup stepKey="loginToAdminPanel" ref="LoginToAdminActionGroup"> + <argument name="adminUser" value="CustomAdminUser"/> + </actionGroup> + ``` + +## Data type usage + +By default, an [`argument`][] expects an entire `entity` when the `type` value is not defined. +There are cases when you use a string instead of a whole entity. + +For example, the following defines the replacement argument `relevantString` using a primitive data type: + +```xml +<actionGroup name="fillExample"> + <arguments> + <argument name="relevantString" defaultValue="defaultString" type="string"/> + </arguments> + <fillField stepKey="fillField1" selector="#input" userInput="{{relevantString}}"/> + <click stepKey="clickSave" selector="#save"/> + <see stepKey="seeItWorked" selector="#outputArea" userInput="{{relevantString}}"/> + <click stepKey="clickParameterizedSelector" selector="{{SomeSection.parameterizedElement(relevantString)}}"/> +</actionGroup> +``` + +The `string` argument type provides a method to pass a single piece of data to the `<actionGroup>`during a test instead of passing an entire entity. + +### Explicitly define the argument value + +```xml +<actionGroup stepKey="fillWithStringLiteral" ref="fillExample"> + <argument name="relevantString" value="overrideString"/> +</actionGroup> +``` + +### Use persisted data references to define the argument value + +```xml +<actionGroup stepKey="fillWithStringLiteral" ref="fillExample"> + <argument name="relevantString" value="$persistedData.field1$"/> +</actionGroup> +``` + +The `relevantString` argument value points to the data [created][] in the `stepKey="persistedData"` test step. +`field1` is a data key of the required data string. +Even with the `persistedData` data entity, the MFTF interprets the `$persistedData.field1$` value as a string. + +### Define the argument value based on data entity resolution + +The argument value points to a piece of data defined in a `data.xml` file. +The `field1` data contains the required string. +MFTF resolves `{{myCustomEntity.field1}}` the same as it would in a `selector` or `userInput` attribute. + +```xml +<actionGroup stepKey="fillWithXmlData" ref="fillExample"> + <argument name="relevantString" value="{{myCustomEntity.field1}}"/> +</actionGroup> +``` + +## Optimizing action group structures + +Structuring properly an action group increases code reusability and readability. + +Starting with an action group such as: + +```xml +<actionGroup name="CreateCategory"> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + <seeInCurrentUrl url="{{AdminCategoryPage.url}}" stepKey="seeOnCategoryPage"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Category" stepKey="seeCategoryPageTitle"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryEntity.name}}" stepKey="enterCategoryName"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryEntity.name_lwr}}" stepKey="enterURLKey"/> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + <seeInTitle userInput="{{categoryEntity.name}}" stepKey="seeNewCategoryPageTitle"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="seeCategoryInTree"/> +</actionGroup> +``` + +It can be reworked into more manageable pieces, as below. These smaller steps are easier to read, update, and reuse. + +```xml +<actionGroup name="GoToCategoryGridAndAddNewCategory"> + <seeInCurrentUrl url="{{AdminCategoryPage.url}}" stepKey="seeOnCategoryPage"/> + <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Category" stepKey="seeCategoryPageTitle"/> +</actionGroup> + +<actionGroup name="FillInBasicCategoryFields"> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryEntity.name}}" stepKey="enterCategoryName"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> + <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryEntity.name_lwr}}" stepKey="enterURLKey"/> +</actionGroup> + +<actionGroup name="SaveAndVerifyCategoryCreation"> + <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + <seeInTitle userInput="{{categoryEntity.name}}" stepKey="seeNewCategoryPageTitle"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="seeCategoryInTree"/> +</actionGroup> +``` + +<!-- {% endraw %} --> + +## Elements reference + +### actionGroups {#actiongroups-tag} + +The `<actionGroups>` element is a root element that contains XML configuration attributes. + +Attribute|Value|Description +---|---|--- +`xmlns:xsi`|`"http://www.w3.org/2001/XMLSchema-instance"`|Tells the XML parser to validate this document against a schema. +`xsi:noNamespaceSchemaLocation`|`"urn:magento:mftf:Test/etc/actionGroupSchema.xsd"`|Relative path to the corresponding schema. + +It may contain one or more `<actionGroup>`. + +### actionGroup {#actiongroup-tag} + +Attribute|Type|Use|Description +---|---|---|--- +`name`|string|required|Identifier of the action group. +`extends`|string|optional|Identifies the action group to extend. + +It may contain `<arguments>`. + +### arguments {#arguments-tag} + +The `<arguments>` element is a wrapper for an array of `<argument>` elements. + +### argument {#argument-tag} + +Attribute|Type|Use|Description +---|---|---|--- +`name`|string|required|Identifier of an argument in the scope of the corresponding action group. +`defaultValue`|string|optional|Provides a default data value. +`type`|Possible values: `string`, `entity` (default).|optional|Defines the argument data type; Defaults to `entity`. + +<!-- Link Definitions --> +[actions]: ./actions.md +[test]: ../test.md +[`argument`]: #argument-tag +[created]: ../data.md#persist-data \ No newline at end of file diff --git a/docs/test/actions.md b/docs/test/actions.md new file mode 100644 index 000000000..4a6c3f48e --- /dev/null +++ b/docs/test/actions.md @@ -0,0 +1,2581 @@ +# Test actions + +Actions in the MFTF allow you to automate different scenarios of Magento user's actions. +They are mostly XML implementations of [Codeception actions](http://codeception.com/docs/modules/WebDriver#Actions). +Some actions drive browser elements, while others use REST APIs. + +## Common attributes + +All `<actions>` contain the following attributes that are useful for merging needs. + +### `stepKey` + +`stepKey` is a required attribute that stores a unique identifier of the action. + +Example test step of the `myAction` action with the `conditionalClickStep1` identifier: + +```xml +<myAction stepKey="conditionalClickStep1"/> +``` + +This step can be referenced within the test using `conditionalClickStep1`. + +The value format should met the following principles: + +* Must be unique within [`<test>`](../test.md#test-tag). +* Naming should be as descriptive as possible: + * Describe the action performed. + * Briefly describe the purpose. + * Describe which data is in use. +* Should be in camelCase with lowercase first letter. +* Should be the last attribute of an element. + +### `skipReadiness` + +`skipReadiness` is an optional flag to skip the readiness check. + +Example test step with skipping the readiness check: + +```xml +<myAction skipReadiness="true" stepKey=""/> +``` + +The flag: + +* cannot be used within action groups. + * Can be used on individual actions inside the action group. + +### `before` and `after` + +`before` and `after` are optional attributes that insert the action into the test while merging. The action will be executed before or after the one set in these attributes. The value here is the `stepKey` of reference action. + +Example with `before`: + +```xml +<myAction before="fillField" stepKey="conditionalClickStep1"/> +``` + +`myAction` will be executed before the action, which has `stepKey="fillField"`. + +Example with `after`: + +```xml +<myAction after="fillField" stepKey="seeResult"/> +``` + +`myAction` will be executed after the action, which has `stepKey="fillField"`. + +## Examples + +<!-- {% raw %} --> + +The following example contains four actions: + +1. [Open the Sign In page for a Customer](#example-step1). +2. [Enter a customer's email](#example-step2). +3. [Enter a customer's password](#example-step3). +4. [Click the Sign In button](#example-step4). + + ```xml + <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> + <fillField userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> + <fillField userInput="$$customer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> + <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + ``` + +### 1. Open the Sign In page for a customer {#example-step1} + +```xml +<amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> +``` + +The Customer Sign In page is declared in the `.../Customer/Page/StorefrontCustomerSignInPage.xml` file. +The given relative URI is declared in `StorefrontCustomerSignInPage.url`. + +Source code (`StorefrontCustomerSignInPage.xml` ): + +```xml +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCustomerSignInPage" url="/customer/account/login/" module="Magento_Customer"> + <section name="StorefrontCustomerSignInFormSection" /> + </page> +</config> +``` + +[`<amOnPage>`](#amonpage) is an action that opens a page for a given URI. It has a key `"amOnSignInPage"` that will be used as a reference for merging needs in other modules. +This action uses the `url` attribute value for the given relative URI to open a browser page. +Here, `url` contains a pointer to a `url` attribute of the `StorefrontCustomerSignInPage`. + +### 2. Enter a customer's email {#example-step2} + +```xml +<fillField userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> +``` + +[`<fillField>`](#fillfield) fills a text field with the given string. + +The customer's email is stored in the `email` parameter of the `customer` entity created somewhere earlier in the test using a [`<createData>`](#createdata) tag. +`userInput` points to that data. + +`selector` points to the field where you enter the data. +A required selector is stored in the `emailField` element of the `StorefrontCustomerSignInFormSection` section. + +This section is declared in `.../Customer/Section/StorefrontCustomerSignInFormSection.xml` file: + +```xml +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontCustomerSignInFormSection"> + <element name="emailField" type="input" selector="#email"/> + <element name="passwordField" type="input" selector="#pass"/> + <element name="signInAccountButton" type="button" selector="#send2" timeout="30"/> + </section> +</config> +``` + +### 3. Enter a customer's password {#example-step3} + +```xml +<fillField userInput="$$customer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> +``` + +This `<action>` is very similar to the `<action>` in a previous step. +The only difference is that different data is assigned to the attributes, which set a field with a password. + +### 4. Click the Sign In button {#example-step4} + +```xml +<click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> +``` + +<!-- {% endraw %} --> + +Here, [`<click>`](#click) performs a click on a button that can be found by the selector that is stored in the `signInAccountButton` of the `StorefrontCustomerSignInFormSection`. + +## Actions returning a variable + +The following test actions return a variable: + +* [grabAttributeFrom](#grabattributefrom) +* [grabCookie](#grabcookie) +* [grabFromCurrentUrl](#grabfromcurrenturl) +* [grabMultiple](#grabmultiple) +* [grabPageSource](#grabpagesource) +* [grabTextFrom](#grabtextfrom) +* [grabValueFrom](#grabvaluefrom) +* [executeJS](#executejs) + +Learn more in [Using data returned by test actions](../data.md#use-data-returned-by-test-actions). + +## Actions handling data entities + +The following test actions handle data entities using [metadata](../metadata.md): + +* [createData](#createdata) +* [deleteData](#deletedata) +* [updateData](#updatedata) +* [getData](#getdata) + +Learn more in [Handling a REST API response](../metadata.md#rest-response). + +## Reference + +The following list contains reference documentation about all action elements available in the MFTF. +If the description of an element does not include a link to Codeception analogue, it means that the action is developed by Magento for specific MFTF needs. + +### acceptPopup + +Accepts the current popup visible on the page. + +See [acceptPopup docs on codeception.com](http://codeception.com/docs/modules/WebDriver#acceptPopup). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Accept the current popup visible on the page. --> +<acceptPopup stepKey="acceptPopup"/> +``` + +### amOnPage + +Opens the page by the URL relative to the one set in the `MAGENTO_BASE_URL` configuration variable. + +See [amOnPage docs on codeception.com](http://codeception.com/docs/modules/WebDriver#amOnPage). + +Attribute|Type|Use|Description +---|---|---|--- +`url`|string|optional| A path to the page relative to the `MAGENTO_BASE_URL`. +`stepKey`|string|required|A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Open the `(baseURL)/admin` page. --> +<amOnPage url="/admin" stepKey="goToLogoutPage"/> +``` + +### amOnSubdomain + +Takes the base URL and changes the subdomain. + +See [amOnSubdomain docs on codeception.com](http://codeception.com/docs/modules/WebDriver#amOnSubdomain). + +Attribute|Type|Use|Description +---|---|---|--- +`url`|string|optional| The name of the subdomain. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +Pre-condition: the current base URL is `https://www.magento.com`. + +```xml +<!-- Change the sub-domain to `https://devdocs.magento.com`. --> +<amOnSubdomain url="devdocs" stepKey="changeSubdomain"/> +<!-- Open the page `https://devdocs.magento.com` --> +<amOnPage url="/" stepKey="goToDataPage"/> +``` + +### amOnUrl + +Opens a page by the absolute URL. + +See [amOnUrl docs on codeception.com](http://codeception.com/docs/modules/WebDriver#amOnUrl). + +Attribute|Type|Use|Description +---|---|---|--- +`url`|string|optional| The absolute URL to be used in subsequent steps. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Set url to be used in the next steps to https://www.magento.com/ --> +<amOnUrl url="https://www.magento.com/" stepKey="amOnUrl"/> +``` + +### appendField + +See [appendField docs on codeception.com](http://codeception.com/docs/modules/WebDriver#appendField). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector used to identify the form field. +`userInput`|string|optional| Value to append to the form field. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Append the "Sample Text" string to the selected input element --> +<appendField userInput="Sample Text" selector="input#name" stepKey="appendSuffix"/> +``` + +### attachFile + +See [attachFile docs on codeception.com](http://codeception.com/docs/modules/WebDriver#attachFile). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional|The selector identifying the corresponding HTML element (`<input type="file">`). +`userInput`|string|optional|The name of attaching file. The file must be placed in the `tests/_data` directory. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Upload a file from the `tests/_data` directory with the `image.png` name to the selected input element. --> +<attachFile userInput="image.png" selector="input#imgUpload" stepKey="uploadFile"/> +``` + +### cancelPopup + +See [cancelPopup docs on codeception.com](http://codeception.com/docs/modules/WebDriver#cancelPopup). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Cancel the current popup visible on the page. --> +<cancelPopup stepKey="cancelPopup"/> +``` + +### checkOption + +See [checkOption docs on codeception.com](http://codeception.com/docs/modules/WebDriver#checkOption). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Ensure the checkbox `<input type="checkbox" id="checkbox" ... >...</input>` is checked. --> +<checkOption selector="input#checkbox" stepKey="checkCheckbox"/> +``` + +### clearField + +Clears a text input field. +Equivalent to using [`<fillField>`](#fillfield) with an empty string. + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|required| The selector identifying the corresponding HTML element to be cleared. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Clear the selected field. --> +<clearField selector="input#name" stepKey="clearField"/> +``` + +### click + +See [click docs on codeception.com](http://codeception.com/docs/modules/WebDriver#click). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Selects an element as a key value array. See [strict locator](http://codeception.com/docs/modules/WebDriver#locating-elements). +`userInput`|string|optional| Data to be sent with the click. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Click the selected button. --> +<click selector="button#clickable" stepKey="clickButton"/> +``` + +```xml +<!-- Click on the "<a href=...>Login</a>" link. --> +<click selectorArray="['link' => 'Login']" stepKey="clickButton2"/> +``` + +### clickWithLeftButton + +See [clickWithLeftButton docs on codeception.com](http://codeception.com/docs/modules/WebDriver#clickWithLeftButton). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Selects an element as a key value array; See [strict locator]. +`x`|string|optional| The x-axis value in pixels for the click location. +`y`|string|optional| The y-axis value in pixels for the click location. +`stepKey`|string|required|A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Left click on the center of the `<button id="clickable" />` element. --> +<clickWithLeftButton selector="button#clickable" stepKey="clickButton1"/> +``` + +```xml +<!-- Left click on the point that is 50 px from the top of the window and 50 px from the left of the window. --> +<clickWithLeftButton x="50" y="50" stepKey="clickButton2"/> +``` + +```xml +<!-- Left click on the point that is 50 px from the top and 50 px from the left of of the `<button id="clickable" />` element.. --> +<clickWithLeftButton selector="button#clickable" x="50" y="50" stepKey="clickButton3"/> +``` + +### clickWithRightButton + +See [clickWithRightButton docs on codeception.com](http://codeception.com/docs/modules/WebDriver#clickWithRightButton). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Selects an element as a key value array; See [strict locator]. +`x`|string|optional| The x-axis value in pixels for the click location. +`y`|string|optional| The y-axis value in pixels for the click location. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Right click on the center of the `<button id="clickable" />` element. --> +<clickWithRightButton selector="button#clickable" stepKey="clickButton1"/> +``` + +```xml +<!-- Right click on the point that is 50 px from the top of the window and 50 px from the left of the window. --> +<clickWithRightButton x="50" y="50" stepKey="clickButton2"/> +``` + +```xml +<!-- Right click on the point that is 50 px from the top and 50 px from the left of of the `<button id="clickable" />` element.. --> +<clickWithRightButton selector="button#clickable" x="50" y="50" stepKey="clickButton3"/> +``` + +### closeAdminNotification + +Remove from the DOM all elements with the CSS classes `.modal-popup` or `.modals-overlay`. + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Remove elements of the `.modal-popup` or `.modals-overlay` CSS classes. --> +<closeAdminNotification stepKey="closeAdminNotification"/> +``` + +### closeTab + +See [closeTab docs on codeception.com](http://codeception.com/docs/modules/WebDriver#closeTab). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Close the active tab. --> +<closeTab stepKey="closeTab"/> +``` + +### comment + +Allows input of a string as a PHP code comment. +This tag is not executed. +It is intended to aid documentation and clarity of tests. + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|required| PHP comment that will be written in generated test file. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +```xml +<!-- Open the specified page and print a comment "I am on the login page" in the log during test execution. --> +<amOnPage url="/login" stepKey="goToLoginPage"/> +<comment userInput="I am on the login page" stepKey="loginPageComment"/> +``` + +### conditionalClick + +Conditionally clicks on an element if, and only if, another element is visible or not. + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the HTML element to be clicked. +`dependentSelector`|string|optional| The selector of the HTML element whose visibility is checked for to activate the click. +`visible`|boolean|optional| Determines whether the conditional click is activated by the element being visible or hidden. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Click on the element with `id="foo"` if the element with `id="bar"` is visible. --> +<conditionalClick selector="#foo" dependentSelector="#bar" visible="true" stepKey="click1"/> +``` + +### createData + +Creates an entity (for example, a category or product). +To create an entity, the MFTF makes a `POST` request to the Magento API according to the [data](../data.md) and [metadata](../metadata.md) of the entity to be created. + +Attribute|Type|Use|Description +---|---|---|--- +`entity`|string|required| Type of entity to be created. +`storeCode`|string|optional| ID of the store within which the data is created. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +It can optionally contain one or more `requiredEntity` child elements. + +#### Example + +```xml +<!-- Create an entity with the "SampleProduct" name. --> +<createData entity="SampleProduct" stepKey="createSampleProduct"/> +``` + +#### requiredEntity + +Specify relationships amongst data to be created. +For example, a complex Product object may contain within it a pointer (an ID) to a complex Category object. + +##### Example + +```xml +<!-- Create an entity with the "SampleCategory" name. --> +<createData entity="SampleCategory" stepKey="createCategory"/> +<!-- Create the "SampleProduct" product in that category. --> +<createData entity="SampleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> +</createData> +``` + +Attribute|Type|Use|Description +---|---|---|--- +`createDataKey`|string|required| Name of the required entity. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### field + +Persists a custom field (as a part of the entity) overriding the matching declaration in static data. +This field is replaced at a top level only (nested values such as custom attributes or extension attributes are not replaced). + +Attribute|Type|Use|Description +---|---|---|--- +`key`|string|required| Name of the field to be replaced or added. + +##### Example + +To overwrite the `name` field in a particular product, specify a field element during its creation. + +```xml +<createData entity="SampleProduct" stepKey="createProduct"> + <field key="name">myCustomProductName</field> +</createData> +``` + +### deleteData + +Delete an entity that was previously created. + +Attribute|Type|Use|Description +---|---|---|--- +`createDataKey`|string|optional| Reference to `stepKey` of the `createData` action . +`url`|string|optional| REST API route to send a DELETE request. +`storeCode`|string|optional| ID of the store from which to delete the data. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +Delete the entity that was previously created using [`createData`](#createdata) in the scope of the [test](../test.md#test-tag). + +1. Create _SampleCategory_: + +```xml +<createData entity="SampleCategory" stepKey="createCategory"/> +``` + +1. Delete _SampleCategory_: + +```xml +<deleteData createDataKey="createCategory" stepKey="deleteCategory"/> +``` + +#### Example of existing data deletion + +Delete an entity using [REST API](https://devdocs.magento.com/redoc/2.3/) request to the corresponding route: + +```xml +<grabFromCurrentUrl regex="/^.+id\/([\d]+)/" stepKey="grabId"/> +<deleteData url="V1/categories/{$grabId}" stepKey="deleteCategory"/> +``` + +### dontSee + +See [the codeception.com documentation for more information about this action](http://codeception.com/docs/modules/WebDriver#dontSee). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Value for the form field. +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Array of selectors to evaluate. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Check that the page does not contain the `<h2 id="title">Sample title</h2>` element. --> +<dontSee userInput="Sample title" selector="h2#title" stepKey="dontSeeTitle"/> +``` + +### dontSeeCheckboxIsChecked + +See [dontSeeCheckboxIsChecked docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeCheckboxIsChecked). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the page does not contain the `<input type="checkbox" id="option1" ... >...</input>` element. --> +<dontSeeCheckboxIsChecked selector="input#option1" stepKey="checkboxNotChecked"/> +``` + +### dontSeeCookie + +See [dontSeeCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeCookie). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Value for the form field. +`parameterArray`|string|optional| Parameters to search for within the cookie. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Verify that there is no cookie with the given name `cookie1`. --> +<dontSeeCookie userInput="cookie1" stepKey="cookie1NotPresent"/> +``` + +```xml +<!-- Verify that there is no cookie with the given name `cookie1` from the domain `www.example.com`. --> +<dontSeeCookie userInput="cookie1" parameterArray="['domainName' => '.example.com']" stepKey="dontSeeCookieInExampleDomain"/> +``` + +### dontSeeCurrentUrlEquals + +See [dontSeeCurrentUrlEquals docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeCurrentUrlEquals). + +Attribute|Type|Use|Description +---|---|---|--- +`url`|string|optional| URL to be compared with the current URL. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the relative URL of the current page does not match `/admin`. --> +<dontSeeCurrentUrlEquals url="/admin" stepKey="notOnAdminPage"/> +``` + +### dontSeeCurrentUrlMatches + +See [dontSeeCurrentUrlMatches docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeCurrentUrlMatches) + +Attribute|Type|Use|Description +---|---|---|--- +`regex`|string|optional| Regular expression against the current URI. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the relative URL of the current page does not match the `~$/users/(\d+)~` regular expression. --> +<dontSeeCurrentUrlMatches regex="~$/users/(\d+)~" stepKey="dontSeeCurrentUrlMatches"/> +``` + +### dontSeeElement + +See [dontSeeElement docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeElement). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`parameterArray`|string|optional| Parameters to search for within the selected element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<div id="box" ... >...</div>` is missing or invisible on the current page. --> +<dontSeeElement selector="div#box" stepKey="dontSeeBox"/> +``` + +### dontSeeElementInDOM + +See [dontSeeElementInDOM docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeElementInDOM). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`parameterArray`|string|optional| Array of parameters to search for within the selector. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<div id="box" ... >...</div>` is completely missing on the current page. --> +<dontSeeElementInDOM selector="div#box" stepKey="dontSeeBoxInDOM"/> +``` + +### dontSeeInCurrentUrl + +See [dontSeeInCurrentUrl docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInCurrentUrl). + +Attribute|Type|Use|Description +---|---|---|--- +`url`|string|optional| String to search for within the current URL. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the url of the current active tab does not contain the string "/users/". --> +<dontSeeInCurrentUrl url="/users/" stepKey="dontSeeInCurrentUrl"/> +``` + +### dontSeeInField + +See [dontSeeInField docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInField). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Array of selectors to be searched. +`userInput`|string|optional| Value for the form field. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<input id="field" ... >...</input>` does not contain the text "Sample text". --> +<dontSeeInField userInput="Sample text" selector="input#field" stepKey="dontSeeInField1"/> +``` + +### dontSeeInFormFields + +See [dontSeeInFormFields docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInFormFields). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`parameterArray`|string|optional| Array of name/value pairs of the form fields to check against. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<form name="myform" ... >...</form>` with the input elements `<input name="input1">...</input>` and `<input name="input2">...</input>`, do not have the values of `value1` and `value2` respectively. --> +<dontSeeInFormFields selector="form[name=myform]" parameterArray="['input1' => 'value1', 'input2' => 'value2']" stepKey="dontSeeInFormFields"/> +``` + +### dontSeeInPageSource + +See [dontSeeInPageSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInPageSource). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Value for the form field. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the page source does not contain the string "Sample text". --> +<dontSeeInPageSource userInput="Sample text" stepKey="dontSeeInPageSource"/> +``` + +### dontSeeInSource + +See [dontSeeInSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInSource). + +Attribute|Type|Use|Description +---|---|---|--- +`html`|string|optional| HTML code to search for within the source code. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the page does not contain the raw source code `<h1>Sample text</h1>`. --> +<dontSeeInSource userInput="<h1>Sample text</h1>" stepKey="dontSeeInSource"/> +``` + +### dontSeeInTitle + +See [dontSeeInTitle docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInTitle). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Value to be located in the page title. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the title of the current active window does not contain the text "Page Title". --> +<dontSeeInTitle userInput="Page Title" stepKey="dontSeeInTitle"/> +``` + +### dontSeeJsError + +Ensure that the current page does not have JavaScript errors. + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify there are no JavaScript errors in the current active window. --> +<dontSeeJsError stepKey="dontSeeJsError"/> +``` + +### dontSeeLink + +See [dontSeeLink docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeLink). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Text of the link field to search for. +`url`|string|optional| Value of the href attribute to search for. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Verify that there is no hyperlink tag on the page with the text "External link". --> +<dontSeeLink userInput="External link" stepKey="dontSeeLink"/> +``` + +```xml +<!-- Verify that there is no hyperlink tag with the text "External link" and the `href` attribute of `/admin`. --> +<dontSeeLink userInput="External link" url="/admin" stepKey="dontSeeAdminLink"/> +``` + +### dontSeeOptionIsSelected + +See [dontSeeOptionIsSelected docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeOptionIsSelected). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding select element. +`userInput`|string|optional| Name of the option to look for. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<select id="myselect" ... >...</select>` does not have the option `option1` selected --> +<dontSeeOptionIsSelected userInput="option1" selector="select#myselect" stepKey="dontSeeOption1"/> +``` + +### doubleClick + +See [doubleClick docs on codeception.com](http://codeception.com/docs/modules/WebDriver#doubleClick). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Click the selected element twice in succession. --> +<doubleClick selector="button#mybutton" stepKey="doubleClickButton"/> +``` + +### dragAndDrop + +See [dragAndDrop docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dragAndDrop). + +Attribute|Type|Use|Description +---|---|---|--- +`selector1`|string|optional|A selector for the HTML element to drag. +`selector2`|string|optional|A selector for the HTML element to drop onto. +`x`|int|optional| X offset applied to drag-and-drop destination. +`y`|int|optional| Y offset applied to drag-and-drop destination. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Click and drag `<div id="block1" ... >...</div>` to the middle of `<div id="block2" ... >...</div>` --> +<dragAndDrop selector1="div#block1" selector2="div#block2" stepKey="dragAndDrop"/> +``` + +```xml +<!-- Click and drag `<div id="block1" ... >...</div>` to the middle of `<div id="block2" ... >...</div>` with a left offset of 50px and top offset of 50px. --> +<dragAndDrop selector1="#block1" selector2="#block2" x="50" y="50" stepKey="dragAndDrop"/> +``` + +### executeInSelenium + +See [executeInSelenium docs on codeception.com](http://codeception.com/docs/modules/WebDriver#executeInSelenium). + +Attribute|Type|Use|Description +---|---|---|--- +`function`|string|optional| Name of Selenium function to run. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Execute the Selenium function `function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {$webdriver->get('http://google.com');}`. --> +<executeInSelenium function="function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) {$webdriver->get('http://google.com');}" stepKey="executeInSelenium"/> +``` + +### executeJS + +See [executeJS docs on codeception.com](http://codeception.com/docs/modules/WebDriver#executeJS). + +Attribute|Type|Use|Description +---|---|---|--- +`function`|string|optional| JavaScript to be executed. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Return the time in seconds since Unix Epoch (January 1, 1970) using the JavaScript Date() function. +To access this value, use `{$returnTime}` in later actions. --> +<executeJS function="return Math.floor(new Date() / 1000);" stepKey="returnTime"/> +``` + +To access this value you would use `{$returnTime}` in later actions. + +### fillField + +See [fillField docs on codeception.com](http://codeception.com/docs/modules/WebDriver#fillField). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Array of name/value pairs with which to populate the form. +`userInput`|string|optional| Value for the form field. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Fill in `<input id="myfield" ... >...</input>` with the text "Sample text". --> +<fillField userInput="Sample text" selector="input#myfield" stepKey="fillField"/> +``` + +### formatMoney + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Value for the money form field. +`locale`|string|optional| The PHP locale value for the store. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +### generateDate + +Generates a date for use in `{$stepKey}` format in other test actions. + +Attribute|Type|Use|Description +---|---|---|--- +`date`|string|required| Date input to parse. Uses the same functionality as the PHP `strtotime()` function. +`format`|string|required| Format in which to save the given date. Uses the same formatting as the PHP `date()` function. +`timezone`|string|optional| Timezone to use when generating date, defaults to `America/Los_Angeles`. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Generate a date that is 1 minute after the current date using Pacific Standard Time. For example "07/11/2020 7:00 AM". +To access this value, use `{$generateDate}` in later actions. --> +<generateDate date="+1 minute" format="m/d/Y g:i A" stepKey="generateDate"/> +``` + +### getData + +Gets an entity (for example, a category), from the Magento API according to the data and metadata of the entity type that is requested. + +Attribute|Type|Use|Description +---|---|---|--- +`storeCode`|string|optional| Identifier of the store from which to get the data. +`index`|integer|optional| The index in the returned data array. +`entity`|string|required| Name of the entity from which to get the data. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Get the product attribute that was created using `<createData stepKey="productAttributeHandle" ... />`. --> +<getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> + <requiredEntity createDataKey="productAttributeHandle"/> +</getData> +``` + +The `ProductAttributeOptionGetter` entity must be defined in the corresponding [data `*.xml`](../data.md). + +This action can optionally contain one or more [requiredEntity](#requiredentity) child elements. + +### grabAttributeFrom + +See [grabAttributeFrom docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabAttributeFrom). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`userInput`|string|optional| Name of tag attribute to grab. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Grab the `title` attribute from `<input id="myinput" ... >...</input>`. +To access this value, use `{$grabAttributeFromInput}` in later actions. --> +<grabAttributeFrom userInput="title" selector="input#myinput" stepKey="grabAttributeFromInput"/> +``` + +### grabCookie + +See [grabCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabCookie). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Name of the cookie to grab. +`parameterArray`|string|optional| Array of cookie parameters to grab. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Grab the cookie with the given name `cookie1`. +To access this value, use `{$grabCookie1}` in later actions. --> +<grabCookie userInput="cookie1" stepKey="grabCookie1"/> +``` + +```xml +<!-- Grab the cookie with the given name `cookie1` from the domain `www.example.com`. +To access this value, use `{$grabCookieExampleDomain}` in later actions. --> +<grabCookie userInput="cookie1" parameterArray="['domainName' => '.example.com']" stepKey="grabCookieExampleDomain"/> +``` + +### grabFromCurrentUrl + +See [grabFromCurrentUrl docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabFromCurrentUrl).. + +Attribute|Type|Use|Description +---|---|---|--- +`regex`|string|optional| Regular expression against the current URI. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Grab the text from the current URL that matches the regex expression `~$/user/(\d+)/~`. +To access this value, use `{$grabFromCurrentUrl}` in later actions. --> +<grabFromCurrentUrl regex="~$/user/(\d+)/~" stepKey="grabFromCurrentUrl"/> +``` + +### grabMultiple + +See [grabMultiple docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabMultiple).. + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`userInput`|string|optional| Name of the tag attribute to grab. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Grab every element on the page with the class `myElement` and return them as an array. +To access this value, use `{$grabAllMyElements}` in later actions. --> +<grabMultiple selector="div.myElement" stepKey="grabAllMyElements"/> +``` + +```xml +<!-- Grab the `href` tag from every `a` element on the page and return them as an array. +To access this value, use `{$grabAllLinks}` in later actions. --> +<grabMultiple userInput="href" selector="a" stepKey="grabAllLinks"/> +``` + +### grabPageSource + +See [grabPageSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabPageSource). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Store the page source code as text +To access this value, use `{$grabPageSource}` in later actions. --> +<grabPageSource stepKey="grabPageSource"/> +``` + +### grabTextFrom + +See [grabTextFrom docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabTextFrom). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Store the text currently displayed by the selected element. +To access this value, use `{$grabTitle}` in later actions. --> +<grabTextFrom selector="h2#title" stepKey="grabTitle"/> +``` + +### grabValueFrom + +See [grabValueFrom docs on codeception.com](https://codeception.com/docs/modules/WebDriver#grabValueFrom). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Array of selectors for the form fields to be selected. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Store the value currently entered in <input id="name" ... >...</input>. +To access this value, use `{$grabInputName}` in later actions. --> +<grabValueFrom selector="input#name" stepKey="grabInputName"/> +``` + +### loadSessionSnapshot + +See [loadSessionSnapshot docs on codeception.com](http://codeception.com/docs/modules/WebDriver#loadSessionSnapshot). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Name of saved cookie. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Load all cookies saved via `<saveSessionSnapshot name="savedSnapshot" ... />`. +To access this value, use the `loadSessionSnapshot` action --> +<loadSessionSnapshot userInput="savedSnapshot" stepKey="loadSnapshot"/> +``` + +### magentoCLI + +Specifies a CLI command to execute in a Magento environment. + +Attribute|Type|Use|Description +---|---|---|--- +`command`|string |optional| CLI command to be executed in Magento environment. +`arguments`|string |optional| Unescaped arguments to be passed in with the CLI command. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Re-index all indices via the command line. --> +<magentoCLI command="indexer:reindex" stepKey="reindex"/> +``` + +### makeScreenshot + +See [makeScreenshot docs on codeception.com](http://codeception.com/docs/modules/WebDriver#makeScreenshot). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Name of PNG file to be created. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +Note that the makeScreenshot action does not automatically add the screenshot to Allure reports. + +#### Example + +```xml +<!-- Take a screenshot of the page and save it to the directory `tests/_output/debug` under the name `example.png`. --> +<makeScreenshot userInput="example" stepKey="screenshotPage"/> +``` + +<div class="bs-callout bs-callout-info"> +This action does not add a screenshot to the Allure [report](../reporting.md).</div> + +### maximizeWindow + +See [maximizeWindow docs on codeception.com](http://codeception.com/docs/modules/WebDriver#maximizeWindow). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Maximize the current window. --> +<maximizeWindow stepKey="maximizeWindow"/> +``` + +### moveBack + +See [moveBack docs on codeception.com](http://codeception.com/docs/modules/WebDriver#moveBack). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Move back one page in history. --> +<moveBack stepKey="moveBack"/> +``` + +### moveForward + +See [moveForward docs on codeception.com](http://codeception.com/docs/modules/WebDriver#moveForward).. + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +```xml +<!-- Move forward one page in history. --> +<moveForward stepKey="moveForward"/> +``` + +### moveMouseOver + +See [moveMouseOver docs on codeception.com](http://codeception.com/docs/modules/WebDriver#moveMouseOver). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Array of selectors. +`x`|string|optional| Number of pixels on the x-axis to offset from the selected element. +`y`|string|optional| Number of pixels on the y-axis to offset from the selected element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Move the mouse cursor over the selected element. --> +<moveMouseOver selector="button#product1" stepKey="hoverOverProduct1"/> +``` + +```xml +<!-- Move the mouse cursor over the selected element with an offset of 50px from the top and 50px from the left. --> +<moveMouseOver selector="button#product1" x="50" y="50" stepKey="hoverOverProduct2"/> +``` + +### mSetLocale + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Value of the expected locale. +`locale`|string|optional| Number of the locale value to be set. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +### mResetLocale + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +### openNewTab + +See [openNewTab docs on codeception.com](http://codeception.com/docs/modules/WebDriver#openNewTab). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Open and switch to a new browser tab. --> +<openNewTab stepKey="openNewTab"/> +``` + +### parseFloat + +Parses float number with thousands separator. + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Float value to be parsed. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +### pauseExecution + +See [pauseExecution docs on codeception.com](http://codeception.com/docs/modules/WebDriver#pauseExecution). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Halt test execution until the `enter` key is pressed to continue. --> +<pauseExecution stepKey="pause"/> +``` + +### performOn + +See [performOn docs on codeception.com](http://codeception.com/docs/modules/WebDriver#performOn). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`function`|string|optional| Function or actions to be taken on the selected element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +### pressKey + +See [pressKey docs on codeception.com](http://codeception.com/docs/modules/WebDriver#pressKey). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`userInput`|string|optional| Key to be pressed. +`parameterArray`|string|optional| Array of keys to be pressed and functions to be run for the action. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Press the `a` key within the selected area. --> +<pressKey userInput="a" selector="#targetElement" stepKey="pressA"/> +``` + +The `parameterArray` attribute value must begin with `[` and end with `]`. +To press more than one key at a time, wrap the keys in secondary `[]`. + +```xml +<!-- Press the delete within the selected area uses key constants from the WebDriverKeys class. --> +<pressKey selector="#targetElement" parameterArray="[['ctrl', 'a'], \Facebook\WebDriver\WebDriverKeys::DELETE]" stepKey="pressDelete"/> +``` + +### reloadPage + +See [reloadPage docs on codeception.com](http://codeception.com/docs/modules/WebDriver#reloadPage). + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Reload the current page. --> +<reloadPage stepKey="reloadPage"/> +``` + +### remove + +Removes action by its `stepKey`. + +Attribute|Type|Use|Description +---|---|---|--- +`keyForRemoval`|string|required| Set `stepKey` of the action you want to remove. + +#### Example + +```xml +<!-- Remove an action in the test with the stepKey of `stepKeyToRemove`. --> +<remove keyForRemoval="stepKeyToRemove"/> +``` + +### resetCookie + +See [resetCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#resetCookie). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Name of the cookie to be reset. +`parameterArray`|string|optional| Array of key/values to get reset within the cookie. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Reset a cookie with the name `cookie1`. --> +<resetCookie userInput="cookie1" stepKey="resetCookie1"/> +``` + +```xml +<!-- Reset a cookie with the given name `cookie1` from the domain `www.example.com`. --> +<resetCookie userInput="cookie1" parameterArray="['domainName' => '.example.com']" stepKey="resetCookieExampleDomain"/> +``` + +### resizeWindow + +See [resizeWindow docs on codeception.com](http://codeception.com/docs/modules/WebDriver#resizeWindow). + +Attribute|Type|Use|Description +---|---|---|--- +`width`|string|optional| The new width of the window in pixels. +`height`|string|optional| The new height of the window in pixels. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Resize the current window to a width of 800px and a height of 600px. --> +<resizeWindow width="800" height="600" stepKey="resizeWindow"/> +``` + +### saveSessionSnapshot + +See [saveSessionSnapshot docs on codeception.com](http://codeception.com/docs/modules/WebDriver#saveSessionSnapshot). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Name of snapshot where cookies are to be saved. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Save all of the current cookies under the name `savedSnapshot`. --> +<saveSessionSnapshot userInput="savedSnapshot" stepKey="saveCurrentCookies"/> +``` + +### scrollTo + +See [scrollTo docs on codeception.com](http://codeception.com/docs/modules/WebDriver#scrollTo). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Array of selectors to return. +`x`|string|optional| x offset of the element to be scrolled to. +`y`|string|optional| y offset of the element to be scrolled to. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Move the page to the middle of the selected area. --> +<scrollTo selector="div#anchor" stepKey="scrollToAnchor"/> +``` + +```xml +<!-- Move the page to the middle of the selected area with an offset of 50px from the top and 50px from the left. --> +<scrollTo selector="div#anchor" x="50" y="50" stepKey="scrollToAnchor2"/> +``` + +### scrollToTopOfPage + +A convenience function that executes `window.scrollTo(0,0)` as JavaScript, thus returning to the top of the page. + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Move the page to the uppermost, leftmost position. --> +<scrollToTopOfPage stepKey="scrollToTopOfPages"/> +``` + +### searchAndMultiSelectOption + +Search for and select options from a Magento multi-select drop-down menu. +For example, the drop-down menu you use to assign Products to Categories. + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|required|The selector of a multi select HTML element (drop-down menu). +`parameterArray`|array|required| Items to search and select in the selected drop-down menu. +`requiredAction`|boolean|optional|Clicks **Done** after selections if `true`. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Search and select for "Item 1" amd "Item 2" in the Magento multiselect element with the id of `multiSelect`. --> +<searchAndMultiSelectOption selector="#multiSelect" parameterArray="['Item 1', 'Item 2']" stepKey="searchAndMultiSelect1"/> +``` + +On this test step the MFTF: + +1. Searches for a drop-down HTML element that matches the `#stuff` selector. +2. Opens the drop-down menu. +3. Enters **Item 1** in a search field of the drop-down element. +4. Selects first element from the filtered results. +5. Enters **Item 2** in a search field of the drop-down element. +6. Selects first element from the filtered results. + +### see + +See [see docs on codeception.com](http://codeception.com/docs/modules/WebDriver#see). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| The text to be searched for within the selector. +`selector`|string|optional| The selector identifying the corresponding HTML element to be searched for. +`selectorArray`|string|optional| Array of selectors to be searched for. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the selected element contains the text "Sample title". --> +<see userInput="Sample title" selector="h2#title" stepKey="seeTitle"/> +``` + +### seeCheckboxIsChecked + +See [seeCheckboxIsChecked docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeCheckboxIsChecked). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify `<input type="checkbox" id="option1" ... >...</input>` is checked. --> +<seeCheckboxIsChecked selector="input#option1" stepKey="seeCheckboxChecked"/> +``` + +### seeCookie + +See [seeCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeCookie). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Name of the cookie to be searched for. +`parameterArray`|string|optional| Cookie parameters to be searched for. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Verify that there is a cookie with the given name `cookie1`. --> +<seeCookie userInput="cookie1" stepKey="cookie1Present"/> +``` + +```xml +<!-- Verify that there is a cookie with the given name `cookie1` from the domain `www.example.com`. --> +<seeCookie userInput="cookie1" parameterArray="['domainName' => 'www.example.com']" stepKey="seeCookieInExampleDomain"/> +``` + +### seeCurrentUrlEquals + +See [seeCurrentUrlEquals docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeCurrentUrlEquals). + +Attribute|Type|Use|Description +---|---|---|--- +`url`|string|optional| The full URL to be searched for. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the relative URL of the current page matches `/admin`. --> +<seeCurrentUrlEquals url="/admin" stepKey="onAdminPage"/> +``` + +### seeCurrentUrlMatches + +See [seeCurrentUrlMatches docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeCurrentUrlMatches). + +Attribute|Type|Use|Description +---|---|---|--- +`regex`|string|optional| Regular expression against the current URI. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the relative URL of the current page matches the `~$/users/(\d+)~` regular expression. --> +<seeCurrentUrlMatches regex="~$/users/(\d+)~" stepKey="seeCurrentUrlMatches"/> +``` + +### seeElement + +See [seeElement docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeElement). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Array of selectors to be searched for. +`parameterArray`|string|optional| Array of parameters to be searched for within the selector. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<div id="box" ... >...</div>` is available and visible on the current page. --> +<seeElement selector="div#box" stepKey="seeBox"/> +``` + +### seeElementInDOM + +See [seeElementInDOM docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeElementInDOM). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`parameterArray`|string|optional| Array of parameters to be searched for within the selected element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<div id="box" ... >...</div>` is available on the current page. --> +<seeElementInDOM selector="div#box" stepKey="seeBoxInDOM"/> +``` + +### seeInCurrentUrl + +See [seeInCurrentUrl docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInCurrentUrl). + +Attribute|Type|Use|Description +---|---|---|--- +`url`|string|optional| String to be searched for within the current URL. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the url of the current active tab contains the string "/users/". --> +<seeInCurrentUrl url="/users/" stepKey="seeInCurrentUrl"/> +``` + +### seeInField + +See [seeInField docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInField). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`selectorArray`|string|optional| Array of selectors to be searched. +`userInput`|string|optional| Value to be searched for within the selected form field. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<input id="field" ... >...</input>` contains the text "Sample text". --> +<seeInField userInput="Sample text" selector="input#field" stepKey="seeInField"/> +``` + +### seeInFormFields + +See [seeInFormFields docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInFormFields). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`parameterArray`|string|optional| Array of parameters to be searched for within the selector. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<form name="myform" ... >...</form>` with the input elements `<input name="input1">...</input>` and `<input name="input2">...</input>`, has the values of `value1` and `value2` respectively. --> +<seeInFormFields selector="form[name=myform]" parameterArray="['input1' => 'value1', 'input2' => 'value2']" stepKey="seeInFormFields"/> +``` + +### seeInPageSource + +See [seeInPageSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInPageSource). + +Attribute|Type|Use|Description +---|---|---|--- +`html`|string|optional| HTML code to be searched for within the document. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the page source contains the string "Sample text". --> +<seeInPageSource userInput="Sample text" stepKey="seeInPageSource"/> +``` + +### seeInPopup + +See [seeInPopup docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInPopup). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| String to be searched for within the popup. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify the current popup on the page contains the string "Sample text". --> +<seeInPopup userInput="Sample text" stepKey="seeInPopup"/> +``` + +### seeInSource + +See [seeInSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInSource). + +Attribute|Type|Use|Description +---|---|---|--- +`html`|string|optional| HTML code to be searched for within the page source. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the page does contains the raw source code `<h1>Sample text</h1>`. --> +<seeInSource userInput="<h1>Sample text</h1>" stepKey="seeInSource"/> +``` + +### seeInTitle + +See [seeInTitle docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInTitle). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| String to be searched for within the current page title. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that the title of the current active window contains the text "Page Title". --> +<seeInTitle userInput="Page Title" stepKey="seeInTitle"/> +``` + +### seeLink + +See [seeLink docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeLink). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| String to be searched for within the text of the link. +`url`|string|optional| Hyperlink to be searched. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that there is a hyperlink tag on the page with the text "External link". --> +<seeLink userInput="External link" stepKey="seeLink"/> +``` + +```xml +<!-- Verify that there is a hyperlink tag with the text "External link" and the `href` attribute of `/admin`. --> +<seeLink userInput="External link" url="/admin" stepKey="seeAdminLink"/> +``` + +### seeNumberOfElements + +See [seeNumberOfElements docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeNumberOfElements). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`userInput`|string|optional| Number of instances of the specified selector to be found. +`parameterArray`|string|optional| Array of parameters to be searched for within the selector. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Verify there are 10 `<div id="product" ... >...</div>` elements on the page. --> +<seeNumberOfElements userInput="10" selector="div.product" stepKey="seeTenProducts"/> +``` + +```xml +<!-- Verify there are between 5 and 10 `<div id="product" ... >...</div>` elements on the page. --> +<seeNumberOfElements userInput="[5, 10]" selector=".product" stepKey="seeFiveToTenProducts"/> +``` + +### seeOptionIsSelected + +See [seeOptionIsSelected docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeOptionIsSelected). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`userInput`|string|optional| The name of the option that should be selected. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Verify that `<select id="myselect" ... >...</select>` has the option `option1` selected --> +<seeOptionIsSelected userInput="option1" selector="select#myselect" stepKey="seeOption1"/> +``` + +### selectOption + +See [selectOption docs on codeception.com](http://codeception.com/docs/modules/WebDriver#selectOption). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`userInput`|string|optional| The name of the option to be selected. +`parameterArray`|string|optional| Array of options to be selected. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Select `option1` from `<select id="mySelect" ... >...</select>`. --> +<selectOption userInput="option1" selector="select#mySelect" stepKey="selectOption1"/> +``` + +### selectMultipleOptions + +Selects all given options in the given Magento drop-down element. + +Attribute|Type|Use|Description +---|---|---|--- +`filterSelector`|string|required| The selector for the text filter field. +`optionSelector`|string|required| The selector used to select the corresponding options based on the filter field. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +It contains a child element `<array>` where you specify the options that must be selected using an array format like `['opt1', 'opt2']`. + +#### Example + +```xml +<!-- Select the options `opt1` and `opt2` from `<option class="option" ... >...</option>` and `<input class="filter" ...>...</input>` --> +<selectMultipleOptions filterSelector=".filter" optionSelector=".option" stepKey="selectMultipleOpts1"> + <array>['opt1', 'opt2']</array> +</selectMultipleOptions> +``` + +### setCookie + +See [setCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#setCookie). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| The name of the cookie to be set. +`parameterArray`|string|optional| Array of name/value pairs to be set within the cookie. +`value`|string|optional| Value to be written to the cookie. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Set a cookie with the name of `cookieName` and value of `cookieValue`. --> +<setCookie userInput="cookieName" value="cookieValue" stepKey="setCookie"/> +``` + +### submitForm + +See [submitForm docs on codeception.com](http://codeception.com/docs/modules/WebDriver#submitForm). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`parameterArray`|string|optional| An array of form field names and their corresponding values. +`button`|string|optional| Selector for the form submit button. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Submit a value of `admin` for `<input name="username" ... >...</input>`, a value of `123123q` for `<input name="password" ... >...</input>` for the form `<form id="loginForm" ...>...</form>` and a submit button of `<button id="submit" ... >...</button>` --> +<submitForm selector="#loginForm" parameterArray="['username' => 'admin','password' => '123123q']" button="#submit" stepKey="submitForm"/> +``` + +### switchToIFrame + +See [switchToIFrame docs on codeception.com](http://codeception.com/docs/modules/WebDriver#switchToIFrame). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`userInput`|string|optional| The name of the IFrame to set focus to. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Set the focus to <iframe name="embeddedFrame" ... /> --> +<switchToIFrame userInput="embeddedFrame" stepKey="switchToIFrame"/> +``` + +### switchToNextTab + +See [switchToNextTab docs on codeception.com](http://codeception.com/docs/modules/WebDriver#switchToNextTab). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Offset of the tab to open, usually a number. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Switch to the next tab. --> +<switchToNextTab stepKey="switchToNextTab"/> +``` + +```xml +<!-- Switch to the third next tab. --> +<switchToNextTab userInput="3" stepKey="switchToThirdNextTab"/> +``` + +### switchToPreviousTab + +See [switchToPreviousTab docs on codeception.com](http://codeception.com/docs/modules/WebDriver#switchToPreviousTab). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| Number of tabs to go back. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Examples + +```xml +<!-- Switch to the previous tab. --> +<switchToPreviousTab stepKey="switchToPreviousTab"/> +``` + +```xml +<!-- Switch to the third previous tab. --> +<switchToPreviousTab userInput="3" stepKey="switchToThirdPreviousTab"/> +``` + +### switchToWindow + +See [switchToWindow docs on codeception.com](http://codeception.com/docs/modules/WebDriver#switchToWindow). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| The name of new window to be opened. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Switch to a window with the `name` parameter of `newWindow`. --> +<switchToWindow userInput="newWindow" stepKey="switchToWindow"/> +``` + +### typeInPopup + +See [typeInPopup docs on codeception.com](http://codeception.com/docs/modules/WebDriver#typeInPopup). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| String to be added to the current popup. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Type the text "Sample Text" into the current popup visible on the page. --> +<typeInPopup userInput="Sample Text" stepKey="typeInPopup"/> +``` + +### uncheckOption + +See [uncheckOption docs on codeception.com](http://codeception.com/docs/modules/WebDriver#uncheckOption). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Ensure the checkbox `<input type="checkbox" id="checkbox" ... >...</input>` is unchecked. --> +<uncheckOption selector="input#checkbox" stepKey="uncheckCheckbox"/> +``` + +### unselectOption + +See [unselectOption docs on codeception.com](http://codeception.com/docs/modules/WebDriver#unselectOption). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`userInput`|string|optional| The name of the option to deselect. +`parameterArray`|string|optional| Array of options to be deselected. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Deselect `option1` from `<select id="mySelect" ... >...</select>`. --> +<unselectOption userInput="option1" selector="select#myselect" stepKey="unselectOption1"/> +``` + +### updateData + +When you create a data entity using `createData`, you may need to update it later in the test. +The `updateData` action allows this. + +For example, to change the price of a product: + +```xml +<updateData entity="AdjustPriceProduct" createDataKey="productHandle" stepKey="updateProduct"/> +``` + +Where `AdjustPriceProduct` simply looks like this: + +```xml +<entity name="AdjustPriceProduct" type="product"> + <data key="price">321.00</data> +</entity> +``` + +Only the fields that you want to update are set. + +Attribute|Type|Use|Description +---|---|---|--- +`storeCode`|string|optional| ID of the store in which to apply the updated data. +`entity`|string|required| The name of the `updateData` entity being created. +`createDataKey`|string|required| Key of the data entity to be updated. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +This action can optionally contain one or more [requiredEntity](#requiredentity) child elements. + +### wait + +See [wait docs on codeception.com](http://codeception.com/docs/modules/WebDriver#wait). + +Attribute|Type|Use|Description +---|---|---|--- +`time`|string|optional| The number of seconds to wait. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Halt test execution for 10 seconds before continuing. --> +<wait time="10" stepKey="waitTenSeconds"/> +``` + +### waitForAjaxLoad + +Wait for all AJAX calls to finish. + +Attribute|Type|Use|Description +---|---|---|--- +`time`|string|optional| The number of seconds to wait for Ajax calls to finish. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait up to 30 seconds for all AJAX calls to finish before continuing. --> +<waitForAjaxLoad stepKey="waitForAjaxLoad"/> +``` + +### waitForElementChange + +See [waitForElementChange docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForElementChange). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the HTML element to be changed. +`function`|string|optional| The function to be run after the element changes. +`time`|string|optional| The number of seconds to wait for the change. Default is 30. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait up to 30 seconds for `<div id="changedElement" ... >...</div>` to change to displayed before continuing. --> +<waitForElementChange selector="div#changedElement" function="function(\WebDriverElement $el) {return $el->isDisplayed();}" stepKey="waitForElementChange"/> +``` + +### waitForElement + +See [waitForElement docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForElement). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`time`|string|optional| The number of seconds to wait for the element to appear. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait up to 30 seconds for `<div id="changedElement" ... >...</div>` to be appear on the page before continuing. --> +<waitForElement selector="#changedElement" stepKey="waitForElement"/> +``` + +### waitForElementNotVisible + +See [waitForElementNotVisible docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForElementNotVisible). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`time`|string|optional| The number of seconds to wait for the element to become not visible. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait up to 30 seconds for `<div id="changedElement" ... >...</div>` to become non-visible on the page before continuing. --> +<waitForElementNotVisible selector="#changedElement" stepKey="waitForElementNotVisible"/> +``` + +### waitForElementVisible + +See [waitForElementVisible docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForElementVisible). + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|optional| The selector identifying the corresponding HTML element. +`time`|string|optional| The number of seconds to wait for the element to appear. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait up to 30 seconds for `<div id="changedElement" ... >...</div>` to become visible on the page before continuing. --> +<waitForElementVisible selector="#changedElement" stepKey="waitForElementVisible"/> +``` + +### waitForJS + +See [waitForJS docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForJS). + +Attribute|Type|Use|Description +---|---|---|--- +`function`|string|optional| The function to be run after all JavaScript finishes. +`time`|string|optional| The number of seconds to wait for JavaScript to finish. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait for all jQuery AJAX requests to finish before continuing. --> +<waitForJS function="return $.active == 0;" stepKey="waitForJS"/> +``` + +### waitForLoadingMaskToDisappear + +Wait for all Magento loading overlays to disappear. + +<div class="bs-callout bs-callout-info"> +The CSS class for loading masks is not used consistently throughout Magento. +Therefore, this convenience function tries to wait for various specific selectors.</div> + +```config +# Wait for these classes to not be visible + +//div[contains(@class, "loading-mask")] +//div[contains(@class, "admin_data-grid-loading-mask")] +//div[contains(@class, "admin__data-grid-loading-mask")] +//div[contains(@class, "admin__form-loading-mask")] +//div[@data-role="spinner"] +``` + +Attribute|Type|Use|Description +---|---|---|--- +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait up to 30 seconds for all Magento loading overlays to disappear before continuing. --> +<waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> +``` + +### waitForPageLoad + +Wait for AJAX, Magento loading overlays, and `document.readyState == "complete"`. + +Attribute|Type|Use|Description +---|---|---|--- +`time`|string|optional| Number of seconds to wait for the page to load. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait up to 30 seconds for the current page to fully load before continuing. --> +<waitForPageLoad stepKey="waitForPageLoad"/> +``` + +### waitForPwaElementNotVisible + +Waits up to the given `time` for a PWA Element to disappear from the screen. + +Attribute|Type|Use|Description +---|---|---|--- +`time`|string|optional| Number of seconds to wait for the element to disappear. +`selector`|string|required| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait for the PWA element to disappear. --> +<waitForPwaElementNotVisible time="1" stepKey="waitForPwaElementNotVisible"/> +``` + +### waitForPwaElementVisible + +Waits up to the given 'time' for a PWA Element to appear on the screen. + +Attribute|Type|Use|Description +---|---|---|--- +`time`|string|optional| Number of seconds to wait for the selected element. +`selector`|string|required| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait for the selected element to appear. --> +<waitForPwaElementVisible stepKey="waitForPwaElementVisible"/> +``` + +### waitForText + +See [waitForText docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForText). + +Attribute|Type|Use|Description +---|---|---|--- +`userInput`|string|optional| The string to wait for. +`time`|string|optional| The number of seconds to wait for the text to appear. +`selector`|string|optional| The selector identifying the corresponding HTML element. +`stepKey`|string|required| A unique identifier of the action. +`skipReadiness`|boolean|optional| A flag to skip the readiness check. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of preceding action. + +#### Example + +```xml +<!-- Wait for text "Sample Text" to appear in the selected area before continuing. --> +<waitForText userInput="Sample Text" selector="div#page" stepKey="waitForText"/> +``` diff --git a/docs/test/annotations.md b/docs/test/annotations.md new file mode 100644 index 000000000..f30b8104e --- /dev/null +++ b/docs/test/annotations.md @@ -0,0 +1,233 @@ +# Annotations + + +Annotations are essentially comments in the code. In PHP, they all are marked by a preceding `@` symbol. + +Within [tests], annotations are contained within their own node. + +## Principles + +The following conventions apply to annotations in the Magento Functional Testing Framework (MFTF): + +- All annotations are within an `<annotations>` element. +- Each element within corresponds to a supported annotation type. +- There is no distinction made in XML between Codeception annotations and Allure annotations. +- Each annotation contains only one value. +If multiple annotation values are supported and required each value requires a separate annotation. +- Tests must contain all of the following annotations: stories, title, description, severity. + +Recommended use cases of the annotation types: + +- [stories] - report grouping, a set of tests that verify a story. +- [title] - description of the test purpose. +- [group] - general functionality grouping. +- [description] - description of how the test achieves the purpose defined in the title. +- [skip] - a label for the test to be skipped during generation (for example, an incomplete test blocked by an issue) + +## Example + +```xml +<annotations> + <stories value="Category Creation"/> + <title value="Create a Category via Admin"/> + <description value="Test logs into admin backend and creates a category."/> + <severity value="CRITICAL"/> + <group value="category"/> +</annotations> +``` + +## Reference + +### description + +The `<description>` element is an implementation of a [`@Description`] Allure tag; Metadata for report. + +Attribute|Type|Use +---|---|-- +`value`|string|required + +#### Example + +```xml +<description value="Add Catalog via Admin"/> +``` + +### features + +The `<features>` element is an implementation of a [`@Features`] Allure tag. + +`<features>` sets a string that will be displayed as a feature within the Allure report. Tests under the same feature are grouped together in the report. + +Attribute|Type|Use +---|---|-- +`value`|string|required + +#### Example + +```xml +<features value="Catalog"/> +<features value="Add/Edit"/> +``` + +### group + +The `<group>` element is an implementation of a [`@group`] Codeception tag. + +`<group>` specifies a string to identify and collect tests together. +Any test can be a part of multiple groups. +The purpose of grouping is to create a set of test for a functionality or purpose, such as all cart tests or all slow tests and run them together locally. + +<div class="bs-callout bs-callout-warning" markdown="1"> +Group values cannot collide with [suite][] names. +</div> + +<div class="bs-callout bs-callout-tip" markdown="1"> +Add `<skip>` to the test to skip it during test run. +</div> + +Attribute|Type|Use|Definition +---|---|---|--- +`value`|string|required|A value that is used to group tests. It should be lower case. `skip` is reserved to ignore content of the test and generate an empty test. + +#### Example + +```xml +<group value="category"/> +``` + +### return + +The `<return>` element is an implementation of a [`@return`] Codeception tag. +It specifies what is returned from a test execution. + +Attribute|Type|Use +---|---|-- +`value`|string|required + +#### Example + +```xml +<return value="void"/> +``` + +### severity + +The `<return>` element is an implementation of a [`@Severity`] Allure tag; Metadata for report. + +Attribute|Type|Use|Acceptable values +---|---|---|--- +`value`|string|required|`MINOR`, `AVERAGE`, `MAJOR`, `BLOCKER`, `CRITICAL` + +#### Example + +```xml +<severity value="CRITICAL"/> +``` + +### skip + +Use the `<skip>` element to skip a test. +It contains one or more child elements `<issueId>` to specify one or more issues that cause the test skipping. + +#### issueId + +This element under `<skip>` is required at least once and contains references to issues that cause the test to be skipped. + +Attribute|Type|Use +---|---|-- +`value`|string|required + +#### Example + +```xml +<skip> + <issueId value="#117"/> + <issueId value="MC-345"/> +</skip> +``` + +### stories + +The `<stories>` element is an implementation of a [`@Stories`] Allure tag. +It has the same functionality as [features], within the Story report group. + +Attribute|Type|Use +---|---|-- +`value`|string|required + +#### Example + +```xml +<stories value="Add Catalog"/> +<stories value="Edit Catalog"/> +``` + +### testCaseId + +The `<testCaseId>` element is an implementation of a [`@TestCaseId`] Allure tag. +It specifies a ZephyrId for a test. + +This tag is prefixed to a title of the test annotation to make the test title unique in Allure. + +If the linkage is set up correctly in the Allure config, the test will have a hyperlink to the Zephyr test case in the report. + +Learn more about [setup instructions in Allure]. + +Attribute|Type|Use +---|---|-- +`value`|string|required + +#### Example + +```xml +<testCaseId value="#"/> +``` + +### title + +The `<title>` element is an implementation of [`@Title`] Allure tag; Metadata for report. + +Attribute|Type|Use +---|---|-- +`value`|string|required + +#### Example + +```xml +<title value="Add Catalog"/> +``` + +### useCaseId + +The `<useCaseId>` element is an implementation of a `@UseCaseId` custom tag. It specifies the use case ID for a test and is ignored by Allure configuration at the moment, as Allure implementation is not complete. + +Attribute|Type|Use +---|---|-- +`value`|string|required + +#### Example + +```xml +<useCaseId value="USECASE-1"/> +``` + +<!-- Link definitions --> + +[`@Description`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#extended-test-class-or-test-method-description +[`@Features`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#map-test-classes-and-test-methods-to-features-and-stories +[`@group`]: http://codeception.com/docs/07-AdvancedUsage#Groups +[`@return`]: http://codeception.com/docs/07-AdvancedUsage#Examples +[`@Severity`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#set-test-severity +[`@Stories`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#map-test-classes-and-test-methods-to-features-and-stories +[`@TestCaseId`]: https://github.com/allure-framework/allure1/wiki/Test-Case-ID +[`@Title`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#human-readable-test-class-or-test-method-title +[description]: #description +[features]: #features +[group]: #group +[setup instructions in Allure]: https://github.com/allure-framework/allure1/wiki/Test-Case-ID +[severity]: #severity +[stories]: #stories +[suite]: ../suite.md +[tests]: ../test.md +[title]: #title +[skip]: #skip diff --git a/docs/test/assertions.md b/docs/test/assertions.md new file mode 100644 index 000000000..17a3d194a --- /dev/null +++ b/docs/test/assertions.md @@ -0,0 +1,578 @@ +# Assertions + + +Assertions serve to pass or fail the [test](../test.md#test-tag) if a condition is not met. These assertions will look familiar to you if you've used any other testing framework, like PHPUnit. + +All assertions contain the same [common actions attributes](./actions.md#common-attributes): `stepKey`, `before`, and `after`. + +Most assertions contain a `message` attribute that specifies the text of an informational message to help you identify the cause of the failure. + +## Principles + +The [principles for actions](../test.md#principles) are also applicable to assertions. + +Assertion actions have nested self-descriptive elements, `<expectedResult>` and `<actualResult>`. These elements contain a result type and a value: + +* `type` + * `const` (default) + * `int` + * `float` + * `bool` + * `string` + * `variable` + * `array` +* `value` + +If `variable` is used, the test transforms the corresponding value to `$variable`. Use the `stepKey` of a test, that returns the value you want to use, in assertions: + +`actual="stepKeyOfGrab" actualType="variable"` + +To use variables embedded in a string in `expected` and `actual` of your assertion, use the `{$stepKey}` format: + +`actual="A long assert string {$stepKeyOfGrab} with an embedded variable reference." actualType="variable"` + +## Example + +The following example shows a common test that gets text from a page and asserts that it matches what we expect to see. If it does not, the test will fail at the assert step. + +```xml +<!-- Grab a value from the page using any grab action --> +<grabTextFrom selector="#elementId" stepKey="stepKeyOfGrab"/> + +<!-- Ensure that the value we grabbed matches our expectation --> +<assertEquals message="This is an optional human readable hint that will be shown in the logs if this assert fails." stepKey="assertEquals1"> + <expectedResult type="string">Some String</expectedResult> + <actualResult type="string">A long assert string {$stepKeyOfGrab} with an embedded variable reference.</actualResult> +</assertEquals> +``` + +## Elements reference + +### assertElementContainsAttribute + +Example: + +```xml +<assertElementContainsAttribute selector=".admin__menu-overlay" attribute="style" expectedValue="color: #333;" stepKey="assertElementContainsAttribute"/> +``` + +Attribute|Type|Use|Description +---|---|---|--- +`selector`|string|required| +`expectedValue`|string|optional| A value of the expected result. +`attribute`|string|required| +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertArrayIsSorted + +The `<assertArrayIsSorted>` asserts that the array is sorted according to a specified sort order, ascending or descending. + +Example: + +```xml +<assertArrayIsSorted sortOrder="asc" stepKey="assertSorted"> + <array>[1,2,3,4,5,6,7]</array> +</assertArrayIsSorted> +``` + +Attribute|Type|Use|Description +---|---|---|--- +`sortOrder`|Possible values: `asc`, `desc`|required| A sort order to assert on array values. +`stepKey`|string|required| A unique identifier of the test step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +It contains an `<array>` child element that specifies an array to be asserted for proper sorting. +It must be in typical array format like `[1,2,3,4,5]` or `[alpha, brontosaurus, zebra]`. + +### assertArrayHasKey + +See [assertArrayHasKey docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertArrayHasKey) + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertArrayNotHasKey + +See [assertArrayNotHasKey docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertArrayNotHasKey). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertArraySubset + +See [assertArraySubset docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertArraySubset). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`strict`|boolean|optional| +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertContains + +See [assertContains docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertContains). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertCount + +See [assertCount docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertCount). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertEmpty + +See [assertEmpty docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertEmpty). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertEquals + +See [assertEquals docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertEquals). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`delta`|string|optional| +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertFalse + +See [assertFalse docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertFalse). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| Actual value. +`actualType`|assertEnum|optional| Type of actual value. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertFileExists + +See [assertFileExists docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertFileExists). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertFileNotExists + +See [assertFileNotExists docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertFileNotExists). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertGreaterOrEquals + +See [assertGreaterOrEquals docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertGreaterOrEquals). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertGreaterThan + +See [assertGreaterThan docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertGreaterThan). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertGreaterThanOrEqual + +See [assertGreaterThanOrEqual docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertGreaterThanOrEqual). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertInstanceOf + +See [assertInstanceOf docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertInstanceOf). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertInternalType + +See [assertInternalType docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertInternalType). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertIsEmpty + +See [assertIsEmpty docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertIsEmpty). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertLessOrEquals + +See [assertLessOrEquals docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertLessOrEquals). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertLessThan + +See [assertLessThan docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertLessThan). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertLessThanOrEqual + +See [assertLessThanOrEqual docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertLessThanOrEqual). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertNotContains + +See [assertNotContains docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotContains). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertNotEmpty + +See [assertNotEmpty docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotEmpty). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertNotEquals + +See [assertNotEquals docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotEquals). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`delta`|string|optional| +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertNotInstanceOf + +See [assertNotInstanceOf docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotInstanceOf). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertNotNull + +See [assertNotNull docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotNull). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertNotRegExp + +See [assertNotRegExp docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotRegExp). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertNotSame + +See [assertNotSame docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotSame). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertNull + +See [assertNull docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNull). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertRegExp + +See [assertRegExp docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertRegExp). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertSame + +See [assertSame docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertSame). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertStringStartsNotWith + +See [assertStringStartsNotWith docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertStringStartsNotWith). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertStringStartsWith + +See [assertStringStartsWith docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertStringStartsWith). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### assertTrue + +See [assertTrue docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertTrue). + +Attribute|Type|Use|Description +---|---|---|--- +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`message`|string|optional|Text of informational message about a cause of failure. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### expectException + +See [expectException docs on codeception.com](http://codeception.com/docs/modules/WebDriver#expectException). + +Attribute|Type|Use|Description +---|---|---|--- +`expected`|string|required| A value of the expected result. +`expectedType`|string|optional| A type of the expected result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`actual`|string|required| A value of the actual result. +`actualType`|string|optional| A type of the actual result. Possible values: `const` (default), `int`, `float`, `bool`, `string`, `variable`, `array`. +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. + +### fail + +See [fail docs on codeception.com](http://codeception.com/docs/modules/WebDriver#fail). + +Attribute|Type|Use|Description +---|---|---|--- +`message`|string|required| +`stepKey`|string|required| A unique identifier of the text step. +`before`|string|optional| `stepKey` of action that must be executed next. +`after`|string|optional| `stepKey` of the preceding action. diff --git a/docs/tips-tricks.md b/docs/tips-tricks.md new file mode 100644 index 000000000..edc1d57bd --- /dev/null +++ b/docs/tips-tricks.md @@ -0,0 +1,450 @@ +# Tips and Tricks + +Sometimes, little changes can make a big difference in your project. Here are some test writing tips to keep everything running smoothly. + +## Actions and action groups + +### Use parameterized selectors in action groups with argument references + +Clarity and readability are important factors in good test writing. +Having to parse through unreadable code can be time consuming. Save time by writing clearly. +The good example clearly shows what the selector arguments refer to. +In the bad example we see two parameters being passed into the selector with little clue as to their purpose. + +**Why?** The next person maintaining the test or extending it may not be able to understand what the parameters are referencing. + +<span style="color:green"> +Good +</span> + +<!-- {% raw %} --> + +```xml +<test> + <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> + <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> + <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> + </actionGroup> +</test> + +<actionGroup name="VerifyOptionInProductStorefront"> + <arguments> + <argument name="attributeCode" type="string"/> + <argument name="optionName" type="string"/> + </arguments> + <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID(attributeCode, optionName)}}" stepKey="verifyOptionExists"/> +</actionGroup> +``` + +<span style="color:red"> +Bad +</span> + +```xml +<test> + <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID($createConfigProductAttribute.default_frontend_label$, $createConfigProductAttributeOption1.option[store_labels][1][label]$)}}" stepKey="verifyOptionExists"/> +</test> +``` + +### Perform the most critical actions first in the `<after>` block + +Perform non-browser driving actions first. These are more likely to succeed as no UI is involved. +In the good example, `magentoCLI` and `deleteData` are run first to ensure a proper state. +In the bad example, we perform some heavy UI steps first. + +**Why?** If something goes wrong there, then the critical `magentoCLI` commands may not get a chance to run, leaving Magento configured incorrectly for any upcoming tests. + +<span style="color:green"> +Good: +</span> + +```xml +<after> + <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setIndexerMode"/> + <magentoCLI command="config:set catalog/frontend/flat_catalog_category 0" stepKey="setFlatCatalogCategory"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> +</after> +``` + +<span style="color:red"> +Bad: +</span> + +```xml +<after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <magentoCLI command="config:set catalog/frontend/flat_catalog_category 0" stepKey="setFlatCatalogCategory"/> + <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setIndexerMode"/> + <actionGroup ref="logout" stepKey="logout"/> +</after> +``` + +### When to use see vs. seeElement + +Use `see` and `seeElement` wisely. +If you need to see some element and verify that the text inside is shown correctly, use the `see` action. +If you need to verify that element present on page, use `seeElement`. +But never use `seeElement` and build a xPath which contains the expected text. + +**Why?** For `see` it will output something similar to this: +`Failed asserting that any element by #some_selector contains text "some_text"` +And for `seeElement` it will output something like this: +`Element by #some_selector is not visible.` +There is a subtle distinction: The first is a failure but it is the desired result: a 'positive failure'. +The second is a proper result of the action. + +<span style="color:green"> +Good: +</span> + +```xml +<see selector="//div[@data-element='content']//p" userInput="SOME EXPECTED TEXT" stepKey="seeSlide1ContentStorefront"/> +``` + +<span style="color:red"> +Bad: +</span> + +```xml +<seeElement selector="//div[@data-element='content']//p[.='SOME EXPECTED TEXT']" stepKey="seeSlide1ContentStorefront"/> +``` + +### Always specify a default value for action group arguments + +Whenever possible, specify a `defaultValue` for action group arguments. + +<span style="color:green"> +GOOD: +</span> + +```xml +<actionGroup name="StorefrontAssertProductImagesOnProductPageActionGroup"> + <arguments> + <argument name="productImage" type="string" defaultValue="Magento_Catalog/images/product/placeholder/image.jpg" /> + </arguments> + <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> + <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> + <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" /> + <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" /> + <waitForPageLoad stepKey="waitForGalleryLoaded" /> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" /> + <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" /> + <waitForPageLoad stepKey="waitForGalleryDisappear" /> +</actionGroup> +``` + +<span style="color:red"> +BAD: +</span> + +```xml +<actionGroup name="StorefrontAssertProductImagesOnProductPageActionGroup"> + <arguments> + <argument name="productImage" type="string" /> + </arguments> + <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> + <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> + <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" /> + <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" /> + <waitForPageLoad stepKey="waitForGalleryLoaded" /> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" /> + <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" /> + <waitForPageLoad stepKey="waitForGalleryDisappear" /> +</actionGroup> +``` + +### Build tests from action groups + +Build your tests using action groups, even if an action group contains a single action. + +**Why?** For extension developers, this will make it easier to extend or customize tests. +Extending a single action group will update all tests that use this group. +This improves maintainability as multiple instances of a failure can be fixed with a single action group update. + +<span style="color:green"> +GOOD: +</span> + +```xml +<test name="NavigateClamberWatchEntityTest"> + <annotations> + <!--some annotations--> + </annotations> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="{{ClamberWatch.url_key}}" /> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductPageActionGroup" stepKey="assertProductName"> + <argument name="productName" value="{{ClamberWatch.name}}" /> + </actionGroup> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="assertProductSku"> + <argument name="productSku" value="{{ClamberWatch.sku}}" /> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPriceOnProductPageActionGroup" stepKey="assertProductPrice"> + <argument name="productPrice" value="{{ClamberWatch.price}}" /> + </actionGroup> + <actionGroup ref="StorefrontAssertProductImagesOnProductPageActionGroup" stepKey="assertProductImage"> + <argument name="productImage" value="{{ClamberWatch.image}}" /> + </actionGroup> +</test> +``` + +<span style="color:red"> +BAD: +</span> + +```xml +<test name="NavigateClamberWatchEntityTest"> + <annotations> + <!--some annotations--> + </annotations> + + <amOnPage url="{{StorefrontProductPage.url(ClamberWatch.url_key)}}" stepKey="openProductPage"/> + <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{ClamberWatch.name}}" stepKey="seeProductName" /> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{ClamberWatch.sku}}" stepKey="seeProductSku" /> + <see selector="{{StorefrontProductInfoMainSection.price}}" userInput="{{ClamberWatch.price}}" stepKey="seeProductPrice" /> + <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> + <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> + <seeElement selector="{{StorefrontProductMediaSection.productImage(ClamberWatch.productImage)}}" stepKey="seeProductImage" /> + <click selector="{{StorefrontProductMediaSection.productImage(ClamberWatch.productImage)}}" stepKey="openFullscreenImage" /> + <waitForPageLoad stepKey="waitForGalleryLoaded" /> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(ClamberWatch.productImage)}}" stepKey="seeFullscreenProductImage" /> +</test> +``` + +### Use descriptive stepKey names + +Make `stepKeys` values as descriptive as possible. +Do not use numbers to make a `stepKey` unique. + +**Why?** This helps with readability and clarity. + +<span style="color:green"> +GOOD: +</span> + +```xml +<click selector="{{StorefrontNavigationSection.topCategory(SimpleSubCategory.name)}}" stepKey="clickSimpleSubCategoryLink" /> +<waitForPageLoad stepKey="waitForSimpleSubCategoryPageLoad" /> +<click selector="{{StorefrontCategoryMainSection.productLinkByHref(SimpleProduct.urlKey)}}" stepKey="clickSimpleProductLink" /> +<waitForPageLoad stepKey="waitForSimpleProductPageLoad" /> + +<!-- Perform some actions / Assert product page --> + +<click selector="{{StorefrontNavigationSection.topCategory(CustomCategory.name)}}" stepKey="clickCustomCategoryLink" /> +<waitForPageLoad stepKey="waitForCustomCategoryPageLoad" /> +<click selector="{{StorefrontCategoryMainSection.productLinkByHref(CustomSimpleProduct.urlKey)}}" stepKey="clickCustomSimpleProductLink" /> +<waitForPageLoad stepKey="waitForCustomSimpleProductPageLoad" /> +``` + +<span style="color:red"> +BAD: +</span> + +```xml +<click selector="{{StorefrontNavigationSection.topCategory(SimpleSubCategory.name)}}" stepKey="clickCategoryLink1" /> +<waitForPageLoad stepKey="waitForPageLoad1" /> +<click selector="{{StorefrontCategoryMainSection.productLinkByHref(SimpleProduct.urlKey)}}" stepKey="clickProductLink1" /> +<waitForPageLoad stepKey="waitForPageLoad2" /> + +<!-- Perform some actions / Assert product page --> + +<click selector="{{StorefrontNavigationSection.topCategory(CustomCategory.name)}}" stepKey="clickCategoryLink2" /> +<waitForPageLoad stepKey="waitForPageLoad3" /> +<click selector="{{StorefrontCategoryMainSection.productLinkByHref(CustomSimpleProduct.urlKey)}}" stepKey="clickProductLink2" /> +<waitForPageLoad stepKey="waitForPageLoad4" /> +``` + +**Exception:** + +Use numbers within `stepKeys` when order is important, such as with testing sort order. + +```xml +<createData entity="BasicMsiStock1" stepKey="createCustomStock1"/> +<createData entity="BasicMsiStock2" stepKey="createCustomStock2"/> +<createData entity="BasicMsiStock3" stepKey="createCustomStock3"/> +<createData entity="BasicMsiStock4" stepKey="createCustomStock4"/> +``` + +## Selectors + +### Use contains() around text() + +When possible, use `contains(text(), 'someTextHere')` rather than `text()='someTextHere'`. +`contains()` ignores whitespace while `text()` accounts for it. + +**Why?** +If you are comparing text within a selector and have an unexpected space, or a blank line above or below the string, `text()` will fail while the `contains(text())` format will catch it. +In this scenario `text()` is more exacting. Use it when you need to be very precise about what is getting compared. + +<span style="color:green"> +GOOD: +</span> + +`//span[contains(text(), 'SomeTextHere')]` + +<span style="color:red"> +BAD: +</span> + +`//span[text()='SomeTextHere']` + +### Build selectors in proper order + +When building selectors for form elements, start with the parent context of the form element. +Then specify the element `name` attribute in your selector to ensure the correct element is targeted. +To build a selector for an input, use the pattern: `{{section_selector}} {{input_selector}}` or for a button: `{{section_selector}} {{button_selector}}` + +**Why?** Traversing the DOM takes a finite amount of time and reducing the scope of the selector makes the selector lookup as efficient as possible. + +Example: + +```xml +<div class="admin__field _required" data-bind="css: $data.additionalClasses, attr: {'data-index': index}, visible: visible" data-index="name"> + <div class="admin__field-label" data-bind="visible: $data.labelVisible"> + <span data-bind="attr: {'data-config-scope': $data.scopeLabel}, i18n: label" data-config-scope="[STORE VIEW]">Product Name</span> + </div> + <div class="admin__field-control" data-bind="css: {'_with-tooltip': $data.tooltip, '_with-reset': $data.showFallbackReset && $data.isDifferedFromDefault}"> + <input class="admin__control-text" type="text" name="product[name]" aria-describedby="notice-EXNI71H" id="EXNI71H" maxlength="255" data-bind=" + attr: { + name: inputName, + placeholder: placeholder, + maxlength: 255}"/> + </div> +</div> +``` + +<span style="color:green"> +GOOD: +</span> + +```xml +<element name="productName" type="input" selector="*[data-index='product-details'] input[name='product[name]']"/> +``` + +<span style="color:red"> +BAD: +</span> + +```xml +<element name="productName" type="input" selector=".admin__field[data-index=name] input"/> +``` + +### Build selectors with appropriate specificity + +Selectors that are too general might sweep up unexpected elements. +When possible, select the first parent tag and then specify the desired element within that selection. + +**Why?** Elements that are overly specific are less flexible and may fail if unexpected DOM changes occur. It also reduces the amount of the DOM it needs to parse. + + <span style="color:green"> +GOOD: +</span> + +```html + form[name='myform'] > input[name='firstname'] + + //*[@id='container'][@class='dashboard-title'] + ``` + + <span style="color:red"> +BAD: +</span> + +```html + input[name='firstname'] + + //*[@id='container']/*[@class='dashboard-advanced-reports']/*[@class='dashboard-advanced- reports-description']/*[@class='dashboard-title'] + ``` + +## General tips + +### Use data references to avoid hardcoded values + +If you need to run a command such as `<magentoCLI command="config:set" />`, do not hardcode paths and values to the command. +Rather, create an appropriate `ConfigData.xml` file, which contains the required parameters for running the command. +It will simplify the future maintanence of tests. + + <span style="color:green"> +GOOD: +</span> + +```xml +<magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> +``` + + <span style="color:red"> +BAD: +</span> + +```xml +<magentoCLI command="config:set customer/captcha/length 3" stepKey="setCaptchaLength" /> +``` + +For example: +[This test][] refers to this [Data file][]. + +### Use descriptive variable names + +Use descriptive variable names to increase readability. +**Why?** It makes the code easier to follow and update. + + <span style="color:green"> +GOOD: +</span> + +```xml +<element name="storeName" type="checkbox" selector="//label[contains(text(),'{{storeName}}')]" parameterized="true"/> +``` + +<span style="color:red"> +BAD: +</span> + +```xml +<element name="storeName" type="checkbox" selector="//label[contains(text(),'{{var1}}')]" parameterized="true"/> +``` + +### Use proper checkbox actions + +When working with input type `checkbox`, do not use the `click` action; use `checkOption` or `uncheckOption` instead. +**Why?** A click does not make it clear what the ending state will be; it will simply toggle the current state. Using the proper actions will ensure the expected state of the checkbox. + +<span style="color:green"> +GOOD: +</span> + +```xml +<checkOption selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> +<uncheckOption selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="unselectSecondWebsite"/> +``` + +<span style="color:red"> +BAD: +</span> + +```xml +<click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> +<click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="unselectSecondWebsite"/> +``` + +<!--{% endraw %}--> + +<!-- Link Definitions --> +[This test]: https://github.com/magento/magento2/blob/2.3-develop/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml#L24 +[Data file]: https://github.com/magento/magento2/blob/2.3-develop/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 000000000..6ad300428 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,61 @@ +# Troubleshooting + +Having a little trouble with the MFTF? See some common errors and fixes below. + +## WebDriver issues + +Troubleshoot your WebDriver issues on various browsers. + +### PhantomJS + +You are unable to upload file input using the MFTF actions and are seeing the following exception: + +```terminal +[Facebook\WebDriver\Exception\NoSuchDriverException] +No active session with ID e56f9260-b366-11e7-966b-db3e6f35d8e1 +``` + +#### Reason + +Use of PhantomJS is not actually supported by the MFTF. + +#### Solution + +For headless browsing, the [Headless Chrome][]{:target="\_blank"} has better compatibility with the MFTF. + +### Chrome + +You are seeing an "unhandled inspector error" exception: + +```terminal +[Facebook\WebDriver\Exception\UnknownServerException] +unknown error: undhandled inspector error: {"code":-32601, "message": +"'Network.deleteCookie' wasn't found"} .... +``` + +![Screenshot with the exception](./img/trouble-chrome232.png) + +#### Reason + +Chrome v62 is in the process of being rolled out, and it causes an error with ChromeDriver v2.32+. + +#### Solution + +Use [ChromeDriver v2.33+][]{:target="\_blank"} and [Selenium Server Standalone v3.6.0+][]{:target="\_blank"} in order to execute tests in Google Chrome v62+. + +### Firefox + +Tests that use the `moveMouseOver` action cause an error when run locally. + +#### Reason + +There's a compatibility issue with Codeception's `moveMouseOver` function and GeckoDriver with Firefox. + +#### Solution + +None yet. Solving this problem is dependent on a GeckoDriver fix. + +<!-- Link Definitions --> +[Headless Chrome]: https://developers.google.com/web/updates/2017/04/headless-chrome +[ChromeDriver v2.33+]: https://chromedriver.storage.googleapis.com/index.html?path=2.33/ +[Selenium Server Standalone v3.6.0+]: http://www.seleniumhq.org/download/ \ No newline at end of file diff --git a/docs/update.md b/docs/update.md new file mode 100644 index 000000000..1a3137eb6 --- /dev/null +++ b/docs/update.md @@ -0,0 +1,80 @@ +# Update the Magento Functional Testing Framework + +<div class="bs-callout bs-callout-info" markdown="1"> +[Find your version][] of the MFTF. + +The latest Magento 2.3 release supports MFTF 2.3.13. +The latest Magento 2.2 release supports MFTF 2.3.8. +</div> + +Magento tests and the framework are stored in different repositories. + +Magento tests are stored in the same repository as the Magento code base. +When you pull changes in the Magento code, you're potentially pulling corresponding tests as well. + +The MFTF is installed separately as a dependency using Composer. +When pulling the latest Magento code, update the corresponding Composer dependencies in the `magento2` root directory. +This ensures that the MFTF is up to date. + +## Update the MFTF from 2.3.x + +To update the MFTF to the latest patch: + +1. Verify that the Magento [WYSIWYG settings][] and [Security settings][] are set appropriately. +1. Check details about backward incompatible changes in the [Changelog][] and update your new or customized tests. +1. Get the latest framework version using Composer: + + ```bash + composer update + ``` + +1. Generate the updated tests: + + ```bash + vendor/bin/mftf generate:tests + ``` + +## Update the MFTF from 2.2 + +To update the MFTF from the previous minor version: + +1. When you update Magento, verify that the Magento [WYSIWYG settings][] and [Security settings][] are set appropriately. +1. Starting at the `magento2/` root directory remove the `vendor/` directory: + + ```bash + rm -rf vendor/ + ``` + +1. Get the latest framework version from the Composer dependencies: + + ```bash + composer install + ``` + +1. Run the `upgrade:tests` using the new command line tool: + + ```bash + vendor/bin/mftf upgrade:tests app + ``` + +1. If you are using Phpstorm, update the urn catalog: + + ```bash + vendor/bin/mftf generate:urn-catalog .idea/ + ``` + +1. Update your own tests, including data, metadata, and so on, if they contain tags that are unsupported in the newer version. + + Check details about backward incompatible changes and review new MFTF release documentation in the [Changelog][]. + +1. Generate newly pulled tests: + + ```bash + vendor/bin/mftf generate:tests + ``` + +<!-- Link Definitions --> +[Changelog]: https://github.com/magento/magento2-functional-testing-framework/blob/master/CHANGELOG.md +[WYSIWYG settings]: getting-started.md#wysiwyg-settings +[Security settings]: getting-started.md#security-settings +[Find your version]: introduction.md#find-your-mftf-version \ No newline at end of file diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 000000000..026cab8f8 --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,58 @@ +# Versioning + +This documemt describes the versioning policy for the Magento Functional Testing Framework (MFTF), including the version numbering schema. + +## Backward Compatibility + +In this context, backward compatibility means that when changes are made to the MFTF, all existing tests still run normally. +If a modification to MFTF forces tests to be changed, this is a backward incompatible change. + +## Find your MFTF version number + +To find the version of MFTF that you are using, run the Magento CLI command: + +```bash +cd <magento_root>/ +vendor/bin/mftf --version +``` + +## Versioning Policy + +MFTF versioning policy follows [Semantic Versioning](https://semver.org/) guidelines. + +### 3-component version numbers + +Version numbering schemes help users to understand the scope of the changes made a new release. + +```tree +X.Y.Z +| | | +| | +-- Backward Compatible changes (Patch release - bug fixes, small additions) +| +---- Backward Compatible changes (Minor release - small new features, bug fixes) ++------ Backward Incompatible changes (Major release - new features and/or major changes) + +``` + +For example: + +- Magento 2 ships with MFTF version 2.3.9 +- A patch is added to fix a bug: 2.3.10 (Increment Z = backward compatible change) +- New action command added: 2.4.0 (Increment Y, set Z to 0 = backward compatible change) +- New action added: 2.4.1 (Increment Z = backward compatible change) +- Major new features added to MFTF to support changes in Magento codebase: 3.0.0. (Increment X, reset Y and Z to 0 = backward incompatible change) + +### Z release - patch + +Patch version **Z** MUST be incremented for a release that introduces only backward compatible changes. + +### Y release - minor + +Minor version **Y** MUST be incremented for a release that introduces new, backward compatible features. +It MUST be incremented if any test or test entity is marked as deprecated. +It MAY include patch level changes. Patch version MUST be reset to 0 when minor version is incremented. + +### X release - major + +Major version **X** MUST be incremented for a release that introduces backward incompatible changes. +A major release can also include minor and patch level changes. +You must reset the patch and minor version to 0 when you change the major version. diff --git a/etc/config/codeception.dist.yml b/etc/config/codeception.dist.yml index da23a20ed..e6b03fab8 100755 --- a/etc/config/codeception.dist.yml +++ b/etc/config/codeception.dist.yml @@ -16,7 +16,7 @@ extensions: - Magento\FunctionalTestingFramework\Allure\Adapter\MagentoAllureAdapter config: Magento\FunctionalTestingFramework\Allure\Adapter\MagentoAllureAdapter: - deletePreviousResults: true + deletePreviousResults: false outputDirectory: allure-results ignoredAnnotations: - env diff --git a/etc/config/command.php b/etc/config/command.php index bc8688c21..047af324a 100644 --- a/etc/config/command.php +++ b/etc/config/command.php @@ -13,12 +13,7 @@ $tokenPassedIn = urldecode($_POST['token']); $command = urldecode($_POST['command']); - - if (!empty($_POST['arguments'])) { - $arguments = urldecode($_POST['arguments']); - } else { - $arguments = null; - } + $arguments = urldecode($_POST['arguments']); // Token returned will be null if the token we passed in is invalid $tokenFromMagento = $tokenModel->loadByToken($tokenPassedIn)->getToken(); diff --git a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php index 747e653e9..65be8ea84 100644 --- a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php +++ b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php @@ -6,11 +6,14 @@ namespace Magento\FunctionalTestingFramework\Allure\Adapter; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; +use Yandex\Allure\Adapter\Model\Step; use Yandex\Allure\Codeception\AllureCodeception; use Yandex\Allure\Adapter\Event\StepStartedEvent; use Yandex\Allure\Adapter\Event\StepFinishedEvent; use Yandex\Allure\Adapter\Event\StepFailedEvent; use Yandex\Allure\Adapter\Event\TestCaseFailedEvent; +use Yandex\Allure\Adapter\Event\TestCaseFinishedEvent; use Codeception\Event\FailEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\StepEvent; @@ -21,10 +24,13 @@ * Extends AllureAdapter to provide further information for allure reports * * @package Magento\FunctionalTestingFramework\Allure + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MagentoAllureAdapter extends AllureCodeception { + const STEP_PASSED = "passed"; + /** * Array of group values passed to test runner command * @@ -98,16 +104,25 @@ private function sanitizeGroupName($group) } /** - * Override of parent method, only different to prevent replacing of . to • + * Override of parent method: + * prevent replacing of . to • + * strips control characters * * @param StepEvent $stepEvent * @return void + * @throws \Yandex\Allure\Adapter\AllureException */ public function stepBefore(StepEvent $stepEvent) { //Hard set to 200; we don't expose this config in MFTF $argumentsLength = 200; - $stepAction = $stepEvent->getStep()->getHumanizedActionWithoutArguments(); + + // DO NOT alter action if actionGroup is starting, need the exact actionGroup name for good logging + if (strpos($stepEvent->getStep()->getAction(), ActionGroupObject::ACTION_GROUP_CONTEXT_START) !== false) { + $stepAction = $stepEvent->getStep()->getAction(); + } else { + $stepAction = $stepEvent->getStep()->getHumanizedActionWithoutArguments(); + } $stepArgs = $stepEvent->getStep()->getArgumentsAsString($argumentsLength); if (!trim($stepAction)) { @@ -117,6 +132,9 @@ public function stepBefore(StepEvent $stepEvent) $stepName = $stepAction . ' ' . $stepArgs; + // Strip control characters so that report generation does not fail + $stepName = preg_replace('/[[:cntrl:]]/', '', $stepName); + $this->emptyStep = false; $this->getLifecycle()->fire(new StepStartedEvent($stepName)); } @@ -148,4 +166,58 @@ public function testIncomplete(FailEvent $failEvent) $message = $e->getMessage(); $this->getLifecycle()->fire($event->withException($e)->withMessage($message)); } + + /** + * Override of parent method, polls stepStorage for testcase and formats it according to actionGroup nesting. + * + * @return void + */ + public function testEnd() + { + // Pops top of stepStorage, need to add it back in after processing + $rootStep = $this->getLifecycle()->getStepStorage()->pollLast(); + $formattedSteps = []; + $actionGroupStepContainer = null; + + foreach ($rootStep->getSteps() as $step) { + // if actionGroup flag, start nesting + if (strpos($step->getName(), ActionGroupObject::ACTION_GROUP_CONTEXT_START) !== false) { + $step->setName(str_replace(ActionGroupObject::ACTION_GROUP_CONTEXT_START, '', $step->getName())); + $actionGroupStepContainer = $step; + continue; + } + // if actionGroup ended, add stack to steps + if (stripos($step->getName(), ActionGroupObject::ACTION_GROUP_CONTEXT_END) !== false) { + $formattedSteps[] = $actionGroupStepContainer; + $actionGroupStepContainer = null; + continue; + } + + if ($actionGroupStepContainer !== null) { + $actionGroupStepContainer->addStep($step); + if ($step->getStatus() !== self::STEP_PASSED) { + // If step didn't pass, need to end action group nesting and set overall step status + $actionGroupStepContainer->setStatus($step->getStatus()); + $formattedSteps[] = $actionGroupStepContainer; + $actionGroupStepContainer = null; + } + } else { + // Add step as normal + $formattedSteps[] = $step; + } + } + + // No public function for setting the step's steps + call_user_func(\Closure::bind( + function () use ($rootStep, $formattedSteps) { + $rootStep->steps = $formattedSteps; + }, + null, + $rootStep + )); + + $this->getLifecycle()->getStepStorage()->put($rootStep); + + $this->getLifecycle()->fire(new TestCaseFinishedEvent()); + } } diff --git a/src/Magento/FunctionalTestingFramework/Console/CommandList.php b/src/Magento/FunctionalTestingFramework/Console/CommandList.php index ce971146e..5bcf1ed31 100644 --- a/src/Magento/FunctionalTestingFramework/Console/CommandList.php +++ b/src/Magento/FunctionalTestingFramework/Console/CommandList.php @@ -11,6 +11,7 @@ /** * Class CommandList has a list of commands. * @codingStandardsIgnoreFile + * @SuppressWarnings(PHPMD) */ class CommandList implements CommandListInterface { @@ -38,6 +39,8 @@ public function __construct(array $commands = []) 'run:failed' => new RunTestFailedCommand(), 'setup:env' => new SetupEnvCommand(), 'upgrade:tests' => new UpgradeTestsCommand(), + 'generate:docs' => new GenerateDocsCommand(), + 'static-checks' => new StaticChecksCommand() ] + $commands; } diff --git a/src/Magento/FunctionalTestingFramework/Console/GenerateDocsCommand.php b/src/Magento/FunctionalTestingFramework/Console/GenerateDocsCommand.php new file mode 100644 index 000000000..db77bc74f --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateDocsCommand.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; +use Magento\FunctionalTestingFramework\Util\DocGenerator; +use PhpParser\Comment\Doc; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class GenerateDocsCommand extends Command +{ + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('generate:docs') + ->setDescription('This command generates documentation for created MFTF files.') + ->addOption( + "output", + 'o', + InputOption::VALUE_REQUIRED, + 'Output Directory' + )->addOption( + "clean", + 'c', + InputOption::VALUE_NONE, + 'Clean Output Directory' + )->addOption( + "force", + 'f', + InputOption::VALUE_NONE, + 'Force Document Generation For All Action Groups' + ); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * @throws TestFrameworkException + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + defined('COMMAND') || define('COMMAND', 'generate:docs'); + $config = $input->getOption('output'); + $clean = $input->getOption('clean'); + $force = $input->getOption('force'); + + MftfApplicationConfig::create( + $force, + MftfApplicationConfig::GENERATION_PHASE, + false, + false + ); + + $allActionGroups = ActionGroupObjectHandler::getInstance()->getAllObjects(); + $docGenerator = new DocGenerator(); + $docGenerator->createDocumentation($allActionGroups, $config, $clean); + + $output->writeln("Generate Docs Command Run"); + + if (empty($config)) { + $output->writeln("Output to ". DocGenerator::DEFAULT_OUTPUT_DIR); + } else { + $output->writeln("Output to ". $config); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php index 097d7dfd8..33a29b2e8 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php @@ -46,12 +46,12 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @return integer|null|void + * @return integer * @throws \Exception * * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $tests = $input->getArgument('name'); $skipGeneration = $input->getOption('skip-generate'); @@ -85,7 +85,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $process->setWorkingDirectory(TESTS_BP); $process->setIdleTimeout(600); $process->setTimeout(0); - $process->run( + + return $process->run( function ($type, $buffer) use ($output) { $output->write($buffer); } diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php index 13f6b8ade..8257162b9 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php @@ -59,13 +59,13 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @return integer|null|void + * @return integer * @throws \Exception * * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { // Create Mftf Configuration MftfApplicationConfig::create( @@ -78,7 +78,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $testConfiguration = $this->getFailedTestList(); if ($testConfiguration === null) { - return null; + // no failed tests found, run is a success + return 0; } $command = $this->getApplication()->find('generate:tests'); @@ -87,7 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $command->run(new ArrayInput($args), $output); $testManifestList = $this->readTestManifestFile(); - + $returnCode = 0; foreach ($testManifestList as $testCommand) { $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional '; $codeceptionCommand .= $testCommand; @@ -96,11 +97,11 @@ protected function execute(InputInterface $input, OutputInterface $output) $process->setWorkingDirectory(TESTS_BP); $process->setIdleTimeout(600); $process->setTimeout(0); - $process->run( + $returnCode = max($returnCode, $process->run( function ($type, $buffer) use ($output) { $output->write($buffer); } - ); + )); if (file_exists(self::TESTS_FAILED_FILE)) { $this->failedList = array_merge( $this->failedList, @@ -111,6 +112,8 @@ function ($type, $buffer) use ($output) { foreach ($this->failedList as $test) { $this->writeFailedTestToFile($test, self::TESTS_FAILED_FILE); } + + return $returnCode; } /** diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php index 3098569fb..e895fb66a 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php @@ -53,12 +53,12 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @return integer|null|void + * @return integer * @throws \Exception * * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $skipGeneration = $input->getOption('skip-generate'); $force = $input->getOption('force'); @@ -102,7 +102,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $process->setWorkingDirectory(TESTS_BP); $process->setIdleTimeout(600); $process->setTimeout(0); - $process->run( + + return $process->run( function ($type, $buffer) use ($output) { $output->write($buffer); } diff --git a/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php b/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php new file mode 100644 index 000000000..fde306791 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php @@ -0,0 +1,56 @@ +<?php +// @codingStandardsIgnoreFile +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Magento\FunctionalTestingFramework\StaticCheck\StaticChecksList; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; + +class StaticChecksCommand extends Command +{ + /** + * Pool of static check scripts to run + * + * @var \Magento\FunctionalTestingFramework\StaticCheck\StaticCheckListInterface + */ + private $staticChecksList; + + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('static-checks') + ->setDescription('This command will run all static checks on xml test materials.'); + $this->staticChecksList = new StaticChecksList(); + } + + /** + * + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null|void + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $staticCheckObjects = $this->staticChecksList->getStaticChecks(); + foreach ($staticCheckObjects as $staticCheck) { + $staticOutput = $staticCheck->execute($input); + LoggingUtil::getInstance()->getLogger(get_class($staticCheck))->info($staticOutput); + $output->writeln($staticOutput); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php index 7305fdc37..7cc9e8a0e 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php @@ -32,6 +32,7 @@ class DataObjectHandler implements ObjectHandlerInterface const _ENTITY_KEY = 'entityKey'; const _SEPARATOR = '->'; const _REQUIRED_ENTITY = 'requiredEntity'; + const _FILENAME = 'filename'; const DATA_NAME_ERROR_MSG = "Entity names cannot contain non alphanumeric characters.\tData='%s'"; /** @@ -134,6 +135,7 @@ private function processParserOutput($parserOutput) $uniquenessData = []; $vars = []; $parentEntity = null; + $filename = $rawEntity[self::_FILENAME] ?? null; if (array_key_exists(self::_DATA, $rawEntity)) { $data = $this->processDataElements($rawEntity); @@ -167,7 +169,8 @@ private function processParserOutput($parserOutput) $linkedEntities, $uniquenessData, $vars, - $parentEntity + $parentEntity, + $filename ); $entityDataObjects[$entityDataObject->getName()] = $entityDataObject; diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php index 66ca3f594..78deef0ef 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php @@ -9,7 +9,6 @@ use Magento\FunctionalTestingFramework\DataGenerator\Objects\OperationElement; use Magento\FunctionalTestingFramework\DataGenerator\Parsers\OperationDefinitionParser; use Magento\FunctionalTestingFramework\DataGenerator\Util\OperationElementExtractor; -use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; use Magento\FunctionalTestingFramework\ObjectManagerFactory; @@ -128,14 +127,12 @@ public function getOperationDefinition($operation, $dataType) * @return void * @throws \Exception * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.UnusedPrivateMethod) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ private function initialize() { - //TODO: Reduce CyclomaticComplexity/NPathComplexity/Length of method, remove warning suppression. $objectManager = ObjectManagerFactory::getObjectManager(); $parser = $objectManager->create(OperationDefinitionParser::class); $parserOutput = $parser->readOperationMetadata()[OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG]; @@ -149,29 +146,11 @@ private function initialize() $returnRegex = $opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_RETURN_REGEX] ?? null; $contentType = $opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_CONTENT_TYPE][0]['value'] ?? null; - $headers = []; - $params = []; + $headers = $this->initializeHeaders($opDefArray); + $params = $this->initializeParams($opDefArray); $operationElements = []; $removeBackend = $opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_BACKEND_REMOVE] ?? false; - if (array_key_exists(OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER, $opDefArray)) { - foreach ($opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER] as $headerEntry) { - if (isset($headerEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE]) - && $headerEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE] !== 'none') { - $headers[] = $headerEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_PARAM] - . ': ' - . $headerEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE]; - } - } - } - - if (array_key_exists(OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM, $opDefArray)) { - foreach ($opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM] as $paramEntry) { - $params[$paramEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_KEY]] = - $paramEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_VALUE]; - } - } - // extract relevant OperationObjects as OperationElements if (array_key_exists(OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT, $opDefArray)) { foreach ($opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT] as $opElementArray) { @@ -241,4 +220,44 @@ private function initialize() ); } } + + /** + * Convert headers metadata into an array of objects for further use in. + * + * @param array $opDefArray + * @return array + */ + private function initializeHeaders(array $opDefArray): array + { + $headers = []; + if (array_key_exists(OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER, $opDefArray)) { + foreach ($opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER] as $headerEntry) { + if (isset($headerEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE]) + && $headerEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE] !== 'none') { + $headers[] = $headerEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_PARAM] + . ': ' + . $headerEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE]; + } + } + } + return $headers; + } + + /** + * Convert params metadata into an array of objects. + * + * @param array $opDefArray + * @return array + */ + private function initializeParams(array $opDefArray): array + { + $params = []; + if (array_key_exists(OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM, $opDefArray)) { + foreach ($opDefArray[OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM] as $paramEntry) { + $params[$paramEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_KEY]] = + $paramEntry[OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_VALUE]; + } + } + return $params; + } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php index 1ba126e5f..070a08bf1 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php @@ -72,6 +72,12 @@ class EntityDataObject */ private $parentEntity; + /** + * String of filename + * @var string + */ + private $filename; + /** * Constructor * @@ -82,9 +88,18 @@ class EntityDataObject * @param string[] $uniquenessData * @param string[] $vars * @param string $parentEntity + * @param string $filename */ - public function __construct($name, $type, $data, $linkedEntities, $uniquenessData, $vars = [], $parentEntity = null) - { + public function __construct( + $name, + $type, + $data, + $linkedEntities, + $uniquenessData, + $vars = [], + $parentEntity = null, + $filename = null + ) { $this->name = $name; $this->type = $type; $this->data = $data; @@ -95,6 +110,7 @@ public function __construct($name, $type, $data, $linkedEntities, $uniquenessDat $this->vars = $vars; $this->parentEntity = $parentEntity; + $this->filename = $filename; } /** @@ -107,6 +123,16 @@ public function getName() return $this->name; } + /** + * Getter for the Entity Filename + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } + /** * Get the type of this entity data object * diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php index 95bcd1bab..4e7915b87 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php @@ -65,13 +65,11 @@ public function __construct($dependentEntities = null) * @param boolean $fromArray * @return array * @throws \Exception - * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function resolveOperationDataArray($entityObject, $operationMetadata, $operation, $fromArray = false) { - //TODO: Refactor to reduce Cyclomatic Complexity, remove SupressWarning accordingly. $operationDataArray = []; self::incrementSequence($entityObject->getName()); @@ -105,80 +103,21 @@ public function resolveOperationDataArray($entityObject, $operationMetadata, $op $operationElementType = $operationElement->getValue(); if (in_array($operationElementType, self::PRIMITIVE_TYPES)) { - $elementData = $this->resolvePrimitiveReference( + $this->resolvePrimitiveReferenceElement( $entityObject, - $operationElement->getKey(), - $operationElement->getType() + $operationElement, + $operationElementType, + $operationDataArray ); - - // If data was defined at all, attempt to put it into operation data array - // If data was not defined, and element is required, throw exception - // If no data is defined, don't input defaults per primitive into operation data array - if ($elementData != null) { - if (array_key_exists($operationElement->getKey(), $entityObject->getUniquenessData())) { - $uniqueData = $entityObject->getUniquenessDataByName($operationElement->getKey()); - if ($uniqueData === 'suffix') { - $elementData .= (string)self::getSequence($entityObject->getName()); - } else { - $elementData = (string)self::getSequence($entityObject->getName()) . $elementData; - } - } - $operationDataArray[$operationElement->getKey()] = $this->castValue( - $operationElementType, - $elementData - ); - } elseif ($operationElement->isRequired()) { - throw new \Exception(sprintf( - self::EXCEPTION_REQUIRED_DATA, - $operationElement->getType(), - $operationElement->getKey(), - $entityObject->getName() - )); - } } 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 - if ($operationElement->isRequired() && $entityNamesOfType == null) { - throw new \Exception(sprintf( - self::EXCEPTION_REQUIRED_DATA, - $operationElement->getType(), - $operationElement->getKey(), - $entityObject->getName() - )); - } - foreach ($entityNamesOfType as $entityName) { - 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; - } else { - $operationDataArray[$operationElement->getKey()] = $operationDataSubArray; - } - } + $this->resolveNonPrimitiveReferenceElement( + $entityObject, + $operation, + $fromArray, + $operationElementType, + $operationElement, + $operationDataArray + ); } } @@ -394,5 +333,106 @@ private function castValue($type, $value) return $newVal; } + + /** + * Resolve a reference for a primitive piece of data + * + * @param EntityDataObject $entityObject + * @param $operationElement + * @param $operationElementType + * @param array $operationDataArray + * @throws TestFrameworkException + */ + private function resolvePrimitiveReferenceElement($entityObject, $operationElement, $operationElementType, array &$operationDataArray) + { + $elementData = $this->resolvePrimitiveReference( + $entityObject, + $operationElement->getKey(), + $operationElement->getType() + ); + + // If data was defined at all, attempt to put it into operation data array + // If data was not defined, and element is required, throw exception + // If no data is defined, don't input defaults per primitive into operation data array + if ($elementData != null) { + if (array_key_exists($operationElement->getKey(), $entityObject->getUniquenessData())) { + $uniqueData = $entityObject->getUniquenessDataByName($operationElement->getKey()); + if ($uniqueData === 'suffix') { + $elementData .= (string)self::getSequence($entityObject->getName()); + } else { + $elementData = (string)self::getSequence($entityObject->getName()) . $elementData; + } + } + $operationDataArray[$operationElement->getKey()] = $this->castValue( + $operationElementType, + $elementData + ); + } elseif ($operationElement->isRequired()) { + throw new \Exception(sprintf( + self::EXCEPTION_REQUIRED_DATA, + $operationElement->getType(), + $operationElement->getKey(), + $entityObject->getName() + )); + } + } + + /** + * Resolves DataObjects referenced by the operation + * + * @param $entityObject + * @param $operation + * @param $fromArray + * @param $operationElementType + * @param $operationElement + * @param array $operationDataArray + * @throws TestFrameworkException + */ + private function resolveNonPrimitiveReferenceElement($entityObject, $operation, $fromArray, &$operationElementType, $operationElement, array &$operationDataArray) + { + $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 + if ($operationElement->isRequired() && $entityNamesOfType == null) { + throw new \Exception(sprintf( + self::EXCEPTION_REQUIRED_DATA, + $operationElement->getType(), + $operationElement->getKey(), + $entityObject->getName() + )); + } + foreach ($entityNamesOfType as $entityName) { + 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; + } else { + $operationDataArray[$operationElement->getKey()] = $operationDataSubArray; + } + } + } // @codingStandardsIgnoreEnd } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php index 1123531ed..2766b39a0 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php @@ -92,7 +92,8 @@ public function extendEntity($entityObject) $newLinkedReferences, $newUniqueReferences, $newVarReferences, - $entityObject->getParentName() + $entityObject->getParentName(), + $entityObject->getFilename() ); return $extendedEntity; } diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php index 98b13fe90..9b75cb10d 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Module; +use Codeception\Module\WebDriver; + /** * Class MagentoPwaActions * @@ -15,8 +17,24 @@ */ class MagentoPwaWebDriver extends MagentoWebDriver { + /** + * Go to the page. + * + * Overriding the MagentoWebDriver version because it contains 'waitForPageLoad'. + * The AJAX check in 'waitForPageLoad' does NOT work with a PWA. + * + * @param string $page + * @throws \Exception + * @return void + */ + public function amOnPage($page) + { + WebDriver::amOnPage($page); + } + /** * Wait for a PWA Element to NOT be visible using JavaScript. + * Add the WAIT_TIMEOUT variable to your .env file for this action. * * @param null $selector * @param null $timeout @@ -25,8 +43,6 @@ class MagentoPwaWebDriver extends MagentoWebDriver */ 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)) { @@ -38,6 +54,7 @@ public function waitForPwaElementNotVisible($selector, $timeout = null) /** * Wait for a PWA Element to be visible using JavaScript. + * Add the WAIT_TIMEOUT variable to your .env file for this action. * * @param null $selector * @param null $timeout @@ -46,8 +63,6 @@ public function waitForPwaElementNotVisible($selector, $timeout = null) */ 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)) { diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php index caf2262fa..d9eec1af1 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php @@ -20,6 +20,7 @@ class PageObjectHandler implements ObjectHandlerInterface const MODULE = 'module'; const PARAMETERIZED = 'parameterized'; const AREA = 'area'; + const FILENAME = 'filename'; const NAME_BLACKLIST_ERROR_MSG = "Page names cannot contain non alphanumeric characters.\tPage='%s'"; /** @@ -56,19 +57,20 @@ private function __construct() if (preg_match('/[^a-zA-Z0-9_]/', $pageName)) { throw new XmlException(sprintf(self::NAME_BLACKLIST_ERROR_MSG, $pageName)); } - $area = $pageData[self::AREA]; - $url = $pageData[self::URL]; + $area = $pageData[self::AREA] ?? null; + $url = $pageData[self::URL] ?? null; if ($area == 'admin') { $url = ltrim($url, "/"); } - $module = $pageData[self::MODULE]; + $module = $pageData[self::MODULE] ?? null; $sectionNames = array_keys($pageData[self::SECTION] ?? []); $parameterized = $pageData[self::PARAMETERIZED] ?? false; + $filename = $pageData[self::FILENAME] ?? null; $this->pageObjects[$pageName] = - new PageObject($pageName, $url, $module, $sectionNames, $parameterized, $area); + new PageObject($pageName, $url, $module, $sectionNames, $parameterized, $area, $filename); } } diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php index b63d5b49b..e77e4c502 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php @@ -22,6 +22,7 @@ class SectionObjectHandler implements ObjectHandlerInterface const LOCATOR_FUNCTION = 'locatorFunction'; const TIMEOUT = 'timeout'; const PARAMETERIZED = 'parameterized'; + const FILENAME = 'filename'; const SECTION_NAME_ERROR_MSG = "Section names cannot contain non alphanumeric characters.\tSection='%s'"; const ELEMENT_NAME_ERROR_MSG = "Element names cannot contain non alphanumeric characters.\tElement='%s'"; @@ -86,7 +87,8 @@ private function __construct() throw new XmlException($exception->getMessage() . " in Section '{$sectionName}'"); } - $this->sectionObjects[$sectionName] = new SectionObject($sectionName, $elements); + $filename = $sectionData[self::FILENAME] ?? null; + $this->sectionObjects[$sectionName] = new SectionObject($sectionName, $elements, $filename); } } diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php index 3533a13f3..d99c40240 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/PageObject.php @@ -58,6 +58,13 @@ class PageObject */ private $area; + /** + * Filename of where the page came from + * + * @var string + */ + private $filename; + /** * PageObject constructor. * @param string $name @@ -66,8 +73,9 @@ class PageObject * @param array $sections * @param boolean $parameterized * @param string $area + * @param string $filename */ - public function __construct($name, $url, $module, $sections, $parameterized, $area) + public function __construct($name, $url, $module, $sections, $parameterized, $area, $filename = null) { $this->name = $name; $this->url = $url; @@ -75,6 +83,7 @@ public function __construct($name, $url, $module, $sections, $parameterized, $ar $this->sectionNames = $sections; $this->parameterized = $parameterized; $this->area = $area; + $this->filename = $filename; } /** @@ -87,6 +96,16 @@ public function getName() return $this->name; } + /** + * Getter for the Page Filename + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } + /** * Getter for Page URL * diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php index 9ac641bca..8c1cac326 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php @@ -25,15 +25,24 @@ class SectionObject */ private $elements = []; + /** + * Filename of where the section came from + * + * @var string + */ + private $filename; + /** * SectionObject constructor. * @param string $name * @param array $elements + * @param string $filename */ - public function __construct($name, $elements) + public function __construct($name, $elements, $filename = null) { $this->name = $name; $this->elements = $elements; + $this->filename = $filename; } /** @@ -46,6 +55,16 @@ public function getName() return $this->name; } + /** + * Getter for the Section Filename + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } + /** * Getter for an array containing all of a section's elements. * diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckInterface.php b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckInterface.php new file mode 100644 index 000000000..45ebdea8d --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +use Symfony\Component\Console\Input\InputInterface; + +/** + * Static check script interface + */ +interface StaticCheckInterface +{ + /** + * Executes static check script, returns output. + * @param InputInterface $input + * @return string + */ + public function execute(InputInterface $input); +} diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckListInterface.php b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckListInterface.php new file mode 100644 index 000000000..a8f1a0f6c --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticCheckListInterface.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\StaticCheck; + +/** + * Contains a list of Static Check Scripts + * @api + */ +interface StaticCheckListInterface +{ + /** + * Gets list of static check script instances + * + * @return \Magento\FunctionalTestingFramework\StaticCheck\StaticCheckListInterface[] + */ + public function getStaticChecks(); +} diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php new file mode 100644 index 000000000..d66d941c6 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +/** + * Class StaticChecksList has a list of static checks to run on test xml + * @codingStandardsIgnoreFile + */ +class StaticChecksList implements StaticCheckListInterface +{ + /** + * Property contains all static check scripts. + * + * @var \Magento\FunctionalTestingFramework\StaticCheck\StaticCheckListInterface[] + */ + private $checks; + + /** + * Constructor + * + * @param array $scripts + */ + public function __construct(array $checks = []) + { + $this->checks = [ + 'testDependencies' => new TestDependencyCheck(), + ] + $checks; + } + + /** + * {@inheritdoc} + */ + public function getStaticChecks() + { + return $this->checks; + } +} diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php new file mode 100644 index 000000000..e0b3cefd6 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php @@ -0,0 +1,386 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +use Magento\FunctionalTestingFramework\Config\Data; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; +use Magento\FunctionalTestingFramework\Page\Handlers\PageObjectHandler; +use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; +use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; +use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Util\ModuleResolver; +use Magento\FunctionalTestingFramework\Util\TestGenerator; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; + +/** + * Class TestDependencyCheck + * @package Magento\FunctionalTestingFramework\StaticCheck + * @SuppressWarnings(PHPMD) + */ +class TestDependencyCheck implements StaticCheckInterface +{ + const EXTENDS_REGEX_PATTERN = '/extends=["\']([^\'"]*)/'; + const ACTIONGROUP_REGEX_PATTERN = '/ref=["\']([^\'"]*)/'; + const ACTIONGROUP_ARGUMENT_REGEX_PATTERN = '/<argument[^\/>]*name="([^"\']*)/'; + + /** + * Array of FullModuleName => [dependencies] + * @var array + */ + private $allDependencies; + + /** + * Array of FullModuleName => [dependencies], including flattened dependency tree + * @var array + */ + private $flattenedDependencies; + + /** + * Array of FullModuleName => PathToModule + * @var array + */ + private $moduleNameToPath; + + /** + * Array of FullModuleName => ComposerModuleName + * @var array + */ + private $moduleNameToComposerName; + + /** + * Transactional Array to keep track of what dependencies have already been extracted. + * @var array + */ + private $alreadyExtractedDependencies; + + /** + * Checks test dependencies, determined by references in tests versus the dependencies listed in the Magento module + * + * @param InputInterface $input + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function execute(InputInterface $input) + { + MftfApplicationConfig::create( + true, + MftfApplicationConfig::UNIT_TEST_PHASE, + false, + false + ); + + ModuleResolver::getInstance()->getModulesPath(); + if (!class_exists('\Magento\Framework\Component\ComponentRegistrar')) { + return "TEST DEPENDENCY CHECK ABORTED: MFTF must be attached or pointing to Magento codebase."; + } + $registrar = new \Magento\Framework\Component\ComponentRegistrar(); + $this->moduleNameToPath = $registrar->getPaths(\Magento\Framework\Component\ComponentRegistrar::MODULE); + $this->moduleNameToComposerName = $this->buildModuleNameToComposerName($this->moduleNameToPath); + $this->flattenedDependencies = $this->buildComposerDependencyList(); + + $allModules = ModuleResolver::getInstance()->getModulesPath(); + $filePaths = [ + DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR . 'ActionGroup' . DIRECTORY_SEPARATOR, + DIRECTORY_SEPARATOR . 'Data' . DIRECTORY_SEPARATOR, + ]; + // These files can contain references to other modules. + $testXmlFiles = $this->buildFileList($allModules, $filePaths[0]); + $actionGroupXmlFiles = $this->buildFileList($allModules, $filePaths[1]); + $dataXmlFiles= $this->buildFileList($allModules, $filePaths[2]); + + $testErrors = []; + $testErrors += $this->findErrorsInFileSet($testXmlFiles); + $testErrors += $this->findErrorsInFileSet($actionGroupXmlFiles); + $testErrors += $this->findErrorsInFileSet($dataXmlFiles); + + //print all errors to file + return $this->printErrorsToFile($testErrors); + } + + /** + * Finds all reference errors in given set of files + * @param Finder $files + * @return array + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + */ + private function findErrorsInFileSet($files) + { + $testErrors = []; + foreach ($files as $filePath) { + $modulePath = dirname(dirname(dirname(dirname($filePath)))); + $moduleFullName = array_search($modulePath, $this->moduleNameToPath) ?? null; + // Not a module, is either dev/tests/acceptance or loose folder with test materials + if ($moduleFullName == null) { + continue; + } + + $contents = file_get_contents($filePath); + $allEntities = []; + preg_match_all(ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN, $contents, $braceReferences); + preg_match_all(self::ACTIONGROUP_REGEX_PATTERN, $contents, $actionGroupReferences); + preg_match_all(self::EXTENDS_REGEX_PATTERN, $contents, $extendReferences); + + // Remove Duplicates + $braceReferences[0] = array_unique($braceReferences[0]); + $actionGroupReferences[1] = array_unique($actionGroupReferences[1]); + $braceReferences[1] = array_unique($braceReferences[1]); + $braceReferences[2] = array_filter(array_unique($braceReferences[2])); + + // Check `data` entities in {{data.field}} or {{data.field('param')}} + foreach ($braceReferences[0] as $reference) { + // trim `{{data.field}}` to `data` + preg_match('/{{([^.]+)/', $reference, $entityName); + // Double check that {{data.field}} isn't an argument for an ActionGroup + $entity = $this->findEntity($entityName[1]); + preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $contents, $possibleArgument); + if (array_search($entityName[1], $possibleArgument[1]) !== false) { + continue; + } + if ($entity !== null) { + $allEntities[$entity->getName()] = $entity; + } + } + + // Drill down into params in {{ref.params('string', $data.key$, entity.reference)}} + foreach ($braceReferences[2] as $parameterizedReference) { + preg_match( + ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER, + $parameterizedReference, + $arguments + ); + $splitArguments = explode(',', ltrim(rtrim($arguments[0], ")"), "(")); + foreach ($splitArguments as $argument) { + // Do nothing for 'string' or $persisted.data$ + if (preg_match(ActionObject::STRING_PARAMETER_REGEX, $argument)) { + continue; + } elseif (preg_match(TestGenerator::PERSISTED_OBJECT_NOTATION_REGEX, $argument)) { + continue; + } + // trim `data.field` to `data` + preg_match('/([^.]+)/', $argument, $entityName); + // Double check that {{data.field}} isn't an argument for an ActionGroup + $entity = $this->findEntity($entityName[1]); + preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $contents, $possibleArgument); + if (array_search($entityName[1], $possibleArgument[1]) !== false) { + continue; + } + if ($entity !== null) { + $allEntities[$entity->getName()] = $entity; + } + } + } + // Check actionGroup references + foreach ($actionGroupReferences[1] as $reference) { + $entity = $this->findEntity($reference); + if ($entity !== null) { + $allEntities[$entity->getName()] = $entity; + } + } + // Check extended objects + foreach ($extendReferences[1] as $reference) { + $entity = $this->findEntity($reference); + if ($entity !== null) { + $allEntities[$entity->getName()] = $entity; + } + } + + $currentModule = $this->moduleNameToComposerName[$moduleFullName]; + $modulesReferencedInTest = $this->getModuleDependenciesFromReferences($allEntities, $currentModule); + $moduleDependencies = $this->flattenedDependencies[$moduleFullName]; + // Find Violations + $violatingReferences = []; + foreach ($modulesReferencedInTest as $entityName => $files) { + $valid = false; + foreach ($files as $module) { + if (array_key_exists($module, $moduleDependencies) || $module == $currentModule) { + $valid = true; + break; + } + } + if (!$valid) { + $violatingReferences[$entityName] = $files; + } + } + + if (!empty($violatingReferences)) { + // Build error output + $errorOutput = "\nFile \"{$filePath->getRealPath()}\""; + $errorOutput .= "\ncontains entity references that violate dependency constraints:\n\t\t"; + foreach ($violatingReferences as $entityName => $files) { + $errorOutput .= "\n\t {$entityName} from module(s): " . implode(", ", $files); + } + $testErrors[$filePath->getRealPath()][] = $errorOutput; + } + } + return $testErrors; + } + + /** + * Builds and returns array of FullModuleNae => composer name + * @param array $array + * @return array + */ + private function buildModuleNameToComposerName($array) + { + $returnList = []; + foreach ($array as $moduleName => $path) { + $composerData = json_decode(file_get_contents($path . DIRECTORY_SEPARATOR . "composer.json")); + $returnList[$moduleName] = $composerData->name; + } + return $returnList; + } + + /** + * Builds and returns flattened dependency list based on composer dependencies + * @return array + */ + private function buildComposerDependencyList() + { + $flattenedDependencies = []; + + foreach ($this->moduleNameToPath as $moduleName => $pathToModule) { + $composerData = json_decode(file_get_contents($pathToModule . DIRECTORY_SEPARATOR . "composer.json"), true); + $this->allDependencies[$moduleName] = $composerData['require']; + } + foreach ($this->allDependencies as $moduleName => $dependencies) { + $this->alreadyExtractedDependencies = []; + $flattenedDependencies[$moduleName] = $this->extractSubDependencies($moduleName); + } + return $flattenedDependencies; + } + + /** + * Recursive function to fetch dependencies of given dependency, and its child dependencies + * @param string $subDependencyName + * @return array + */ + private function extractSubDependencies($subDependencyName) + { + $flattenedArray = []; + + if (array_search($subDependencyName, $this->alreadyExtractedDependencies) !== false) { + return $flattenedArray; + } + + if (isset($this->allDependencies[$subDependencyName])) { + $subDependencyArray = $this->allDependencies[$subDependencyName]; + $flattenedArray = array_merge($flattenedArray, $this->allDependencies[$subDependencyName]); + + // Keep track of dependencies that have already been used, prevents circular dependency problems + $this->alreadyExtractedDependencies[] = $subDependencyName; + foreach ($subDependencyArray as $composerDependencyName => $version) { + $subDependencyFullName = array_search($composerDependencyName, $this->moduleNameToComposerName); + $flattenedArray = array_merge( + $flattenedArray, + $this->extractSubDependencies($subDependencyFullName) + ); + } + } + return $flattenedArray; + } + + /** + * Finds unique array ofcomposer dependencies of given testObjects + * @param array $array + * @return array + */ + private function getModuleDependenciesFromReferences($array) + { + $filenames = []; + foreach ($array as $item) { + // Should it append ALL filenames, including merges? + $allFiles = explode(",", $item->getFilename()); + foreach ($allFiles as $file) { + $modulePath = dirname(dirname(dirname(dirname($file)))); + $fullModuleName = array_search($modulePath, $this->moduleNameToPath); + $composerModuleName = $this->moduleNameToComposerName[$fullModuleName]; + $filenames[$item->getName()][] = $composerModuleName; + } + } + return $filenames; + } + + /** + * Builds list of all XML files in given modulePaths + path given + * @param string $modulePaths + * @param string $path + * @return Finder + */ + private function buildFileList($modulePaths, $path) + { + $finder = new Finder(); + foreach ($modulePaths as $modulePath) { + if (!realpath($modulePath . $path)) { + continue; + } + $finder->files()->in($modulePath . $path)->name("*.xml"); + } + return $finder->files(); + } + + /** + * Attempts to find any MFTF entity by its name. Returns null if none are found. + * @param string $name + * @return mixed + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + */ + private function findEntity($name) + { + if ($name == '_ENV' || $name == '_CREDS') { + return null; + } + + if (DataObjectHandler::getInstance()->getObject($name)) { + return DataObjectHandler::getInstance()->getObject($name); + } elseif (PageObjectHandler::getInstance()->getObject($name)) { + return PageObjectHandler::getInstance()->getObject($name); + } elseif (SectionObjectHandler::getInstance()->getObject($name)) { + return SectionObjectHandler::getInstance()->getObject($name); + } + + try { + return ActionGroupObjectHandler::getInstance()->getObject($name); + } catch (TestReferenceException $e) { + } + try { + return TestObjectHandler::getInstance()->getObject($name); + } catch (TestReferenceException $e) { + } + return null; + } + + /** + * Prints out given errors to file, and returns summary result string + * @param array $errors + * @return string + */ + private function printErrorsToFile($errors) + { + if (empty($errors)) { + return "No Dependency errors found."; + } + $outputPath = getcwd() . DIRECTORY_SEPARATOR . "mftf-dependency-checks.txt"; + $fileResource = fopen($outputPath, 'w'); + $header = "MFTF File Dependency Check:\n"; + fwrite($fileResource, $header); + foreach ($errors as $test => $error) { + fwrite($fileResource, $error[0] . PHP_EOL); + } + fclose($fileResource); + $errorCount = count($errors); + $output = "Dependency errors found across {$errorCount} file(s). Error details output to {$outputPath}"; + return $output; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php index 67c1ba25c..ef2e11c03 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php @@ -7,8 +7,6 @@ namespace Magento\FunctionalTestingFramework\Test\Objects; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; -use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; -use Magento\FunctionalTestingFramework\Test\Util\ActionGroupObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; use Magento\FunctionalTestingFramework\Test\Util\ObjectExtension; @@ -19,6 +17,10 @@ class ActionGroupObject { const ACTION_GROUP_ORIGIN_NAME = "actionGroupName"; const ACTION_GROUP_ORIGIN_TEST_REF = "testInvocationRef"; + const ACTION_GROUP_DESCRIPTION = "description"; + const ACTION_GROUP_PAGE = "page"; + const ACTION_GROUP_CONTEXT_START = "Entering Action Group "; + const ACTION_GROUP_CONTEXT_END = "Exiting Action Group "; const STEPKEY_REPLACEMENT_ENABLED_TYPES = [ "executeJS", "magentoCLI", @@ -64,6 +66,13 @@ class ActionGroupObject */ private $arguments; + /** + * An array used to store annotation information to values + * + * @var array + */ + private $annotations; + /** * String of parent Action Group * @@ -71,15 +80,24 @@ class ActionGroupObject */ private $parentActionGroup; + /** + * Filename where actionGroup came from + * + * @var string + */ + private $filename; + /** * ActionGroupObject constructor. * * @param string $name + * @param array $annotations * @param ArgumentObject[] $arguments * @param array $actions * @param string $parentActionGroup + * @param string $filename */ - public function __construct($name, $arguments, $actions, $parentActionGroup) + public function __construct($name, $annotations, $arguments, $actions, $parentActionGroup, $filename = null) { $this->varAttributes = array_merge( ActionObject::SELECTOR_ENABLED_ATTRIBUTES, @@ -87,9 +105,11 @@ public function __construct($name, $arguments, $actions, $parentActionGroup) ); $this->varAttributes[] = ActionObject::ACTION_ATTRIBUTE_URL; $this->name = $name; + $this->annotations = $annotations; $this->arguments = $arguments; $this->parsedActions = $actions; $this->parentActionGroup = $parentActionGroup; + $this->filename = $filename; } /** @@ -190,6 +210,8 @@ private function getResolvedActionsWithArgs($arguments, $actionReferenceKey) ); } + $resolvedActions = $this->addContextCommentsToActionList($resolvedActions, $actionReferenceKey); + return $resolvedActions; } @@ -409,6 +431,16 @@ public function getName() return $this->name; } + /** + * Getter for the Action Group Filename + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } + /** * Getter for the Parent Action Group Name * @@ -439,6 +471,16 @@ public function getArguments() return $this->arguments; } + /** + * Getter for the Action Group Annotations + * + * @return array + */ + public function getAnnotations() + { + return $this->annotations; + } + /** * Searches through ActionGroupObject's arguments and returns first argument wi * @param string $name @@ -481,4 +523,27 @@ private function replaceCreateDataKeys($action, $replacementStepKeys) return $resolvedActionAttributes; } + + /** + * Adds comment ActionObjects before and after given actionList for context setting. + * @param array $actionList + * @param string $actionReferenceKey + * @return array + */ + private function addContextCommentsToActionList($actionList, $actionReferenceKey) + { + $actionStartComment = self::ACTION_GROUP_CONTEXT_START . $this->name . " (" . $actionReferenceKey . ")"; + $actionEndComment = self::ACTION_GROUP_CONTEXT_END . $this->name . " (" . $actionReferenceKey . ")"; + $startAction = new ActionObject( + $actionStartComment, + ActionObject::ACTION_TYPE_COMMENT, + [ActionObject::ACTION_ATTRIBUTE_USERINPUT => $actionStartComment] + ); + $endAction = new ActionObject( + $actionEndComment, + ActionObject::ACTION_TYPE_COMMENT, + [ActionObject::ACTION_ATTRIBUTE_USERINPUT => $actionEndComment] + ); + return [$startAction->getStepKey() => $startAction] + $actionList + [$endAction->getStepKey() => $endAction]; + } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php index 166a61889..8a3bb27a6 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -60,7 +60,7 @@ class ActionObject const ASSERTION_VALUE_ATTRIBUTE = "value"; const DELETE_DATA_MUTUAL_EXCLUSIVE_ATTRIBUTES = ["url", "createDataKey"]; const EXTERNAL_URL_AREA_INVALID_ACTIONS = ['amOnPage']; - const FUNCTION_CLOSURE_ACTIONS = ['waitForElementChange', 'performOn']; + const FUNCTION_CLOSURE_ACTIONS = ['waitForElementChange', 'performOn', 'executeInSelenium']; const MERGE_ACTION_ORDER_AFTER = 'after'; const MERGE_ACTION_ORDER_BEFORE = 'before'; const ACTION_ATTRIBUTE_TIMEZONE = 'timezone'; @@ -68,7 +68,10 @@ class ActionObject const ACTION_ATTRIBUTE_SELECTOR = 'selector'; const ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER = '/\(.+\)/'; const ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN = '/({{[\w]+\.[\w\[\]]+}})|({{[\w]+\.[\w]+\((?(?!}}).)+\)}})/'; + const STRING_PARAMETER_REGEX = "/'[^']+'/"; const DEFAULT_WAIT_TIMEOUT = 10; + const ACTION_ATTRIBUTE_USERINPUT = 'userInput'; + const ACTION_TYPE_COMMENT = 'comment'; /** * The unique identifier for the action @@ -464,8 +467,6 @@ private function stripAndSplitReference($reference) */ private function stripAndReturnParameters($reference) { - // 'string', or 'string,!@#$%^&*()_+, ' - $literalParametersRegex = "/'[^']+'/"; $postCleanupDelimiter = "::::"; preg_match(ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER, $reference, $matches); @@ -473,8 +474,9 @@ private function stripAndReturnParameters($reference) $strippedReference = ltrim(rtrim($matches[0], ")"), "("); // Pull out all 'string' references, as they can contain 'string with , comma in it' - preg_match_all($literalParametersRegex, $strippedReference, $literalReferences); - $strippedReference = preg_replace($literalParametersRegex, '&&stringReference&&', $strippedReference); + // 'string', or 'string,!@#$%^&*()_+, ' + preg_match_all(self::STRING_PARAMETER_REGEX, $strippedReference, $literalReferences); + $strippedReference = preg_replace(self::STRING_PARAMETER_REGEX, '&&stringReference&&', $strippedReference); // Sanitize 'string, data.field,$persisted.field$' => 'string::::data.field::::$persisted.field$' $strippedReference = preg_replace('/,/', ', ', $strippedReference); diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php new file mode 100644 index 000000000..b6e89fd44 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Test\Util; + +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + +/** + * Class AnnotationExtractor + */ +class ActionGroupAnnotationExtractor extends AnnotationExtractor +{ + const ACTION_GROUP_REQUIRED_ANNOTATIONS = [ + "description", + "page", + ]; + const GENERATE_DOCS_COMMAND = 'generate:docs'; + + /** + * This method trims away irrelevant tags and returns annotations used in the array passed. The annotations + * can be found in both Tests and their child element tests. + * + * @param array $testAnnotations + * @param string $filename + * @return array + * @throws \Exception + */ + public function extractAnnotations($testAnnotations, $filename) + { + $annotationObjects = []; + $annotations = $this->stripDescriptorTags($testAnnotations, parent::NODE_NAME); + + foreach ($annotations as $annotationKey => $annotationData) { + $annotationObjects[$annotationKey] = $annotationData[parent::ANNOTATION_VALUE]; + } + // TODO: Remove this when all action groups have annotations + if ($this->isCommandDefined()) { + $this->validateMissingAnnotations($annotationObjects, $filename); + } + + return $annotationObjects; + } + + /** + * Validates given annotations against list of required annotations. + * + * @param array $annotationObjects + * @return void + * @throws \Exception + */ + private function validateMissingAnnotations($annotationObjects, $filename) + { + $missingAnnotations = []; + + foreach (self::ACTION_GROUP_REQUIRED_ANNOTATIONS as $REQUIRED_ANNOTATION) { + if (!array_key_exists($REQUIRED_ANNOTATION, $annotationObjects)) { + $missingAnnotations[] = $REQUIRED_ANNOTATION; + } + } + + if (!empty($missingAnnotations)) { + $message = "Action Group File {$filename} is missing required annotations."; + LoggingUtil::getInstance()->getLogger(ActionObject::class)->deprecation( + $message, + ["actionGroup" => $filename, "missingAnnotations" => implode(", ", $missingAnnotations)] + ); + } + } + + /** + * Checks if command is defined as generate:docs + * + * @return boolean + */ + private function isCommandDefined() + { + if (defined('COMMAND') and COMMAND == self::GENERATE_DOCS_COMMAND) { + return true; + } else { + return false; + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php index 04f739af6..74665cf75 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php @@ -18,6 +18,7 @@ class ActionGroupObjectExtractor extends BaseObjectExtractor { const DEFAULT_VALUE = 'defaultValue'; const ACTION_GROUP_ARGUMENTS = 'arguments'; + const ACTION_GROUP_ANNOTATIONS = 'annotations'; const FILENAME = 'filename'; const ACTION_GROUP_INSERT_BEFORE = "insertBefore"; const ACTION_GROUP_INSERT_AFTER = "insertAfter"; @@ -30,12 +31,20 @@ class ActionGroupObjectExtractor extends BaseObjectExtractor */ private $actionObjectExtractor; + /** + * Annotation Extractor object + * + * @var AnnotationExtractor + */ + private $annotationExtractor; + /** * ActionGroupObjectExtractor constructor. */ public function __construct() { $this->actionObjectExtractor = new ActionObjectExtractor(); + $this->annotationExtractor = new ActionGroupAnnotationExtractor(); } /** @@ -55,6 +64,7 @@ public function extractActionGroup($actionGroupData) self::NODE_NAME, self::ACTION_GROUP_ARGUMENTS, self::NAME, + self::ACTION_GROUP_ANNOTATIONS, self::FILENAME, self::ACTION_GROUP_INSERT_BEFORE, self::ACTION_GROUP_INSERT_AFTER, @@ -62,6 +72,16 @@ public function extractActionGroup($actionGroupData) ); // TODO filename is now available to the ActionGroupObject, integrate this into debug and error statements + + try { + $annotations = $this->annotationExtractor->extractAnnotations( + $actionGroupData[self::ACTION_GROUP_ANNOTATIONS] ?? [], + $actionGroupData[self::FILENAME] + ); + } catch (\Exception $error) { + throw new XmlException($error->getMessage() . " in Action Group " . $actionGroupData[self::FILENAME]); + } + try { $actions = $this->actionObjectExtractor->extractActions($actionData); } catch (\Exception $error) { @@ -74,9 +94,11 @@ public function extractActionGroup($actionGroupData) return new ActionGroupObject( $actionGroupData[self::NAME], + $annotations, $arguments, $actions, - $actionGroupReference + $actionGroupReference, + $actionGroupData[self::FILENAME] ); } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php index e67037940..91617b1e2 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php @@ -129,9 +129,11 @@ public function extendActionGroup($actionGroupObject) // Create new Action Group object to return $extendedActionGroup = new ActionGroupObject( $actionGroupObject->getName(), + $actionGroupObject->getAnnotations(), $extendedArguments, $newActions, - $actionGroupObject->getParentName() + $actionGroupObject->getParentName(), + $actionGroupObject->getFilename() ); return $extendedActionGroup; } diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd index 619d21e9f..616995531 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd @@ -30,6 +30,14 @@ </xs:sequence> </xs:complexType> </xs:element> + <xs:element name="annotations"> + <xs:complexType> + <xs:sequence> + <xs:element name="description"/> + <xs:element name="page"/> + </xs:sequence> + </xs:complexType> + </xs:element> </xs:choice> <xs:attribute type="xs:string" name="name" use="required"/> <xs:attribute type="xs:string" name="filename"/> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd index 90671c340..9aa772858 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd @@ -37,7 +37,6 @@ <xs:element type="executeInSeleniumType" name="executeInSelenium" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="executeJSType" name="executeJS" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="fillFieldType" name="fillField" minOccurs="0" maxOccurs="unbounded"/> - <xs:element type="fillFieldType" name="fillSecretField" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="loadSessionSnapshotType" name="loadSessionSnapshot" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="makeScreenshotType" name="makeScreenshot" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="maximizeWindowType" name="maximizeWindow" minOccurs="0" maxOccurs="unbounded"/> diff --git a/src/Magento/FunctionalTestingFramework/Util/DocGenerator.php b/src/Magento/FunctionalTestingFramework/Util/DocGenerator.php new file mode 100644 index 000000000..9846cded5 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/DocGenerator.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Test\Util\ActionGroupAnnotationExtractor; +use Magento\FunctionalTestingFramework\Test\Util\ActionGroupObjectExtractor; + +/** + * Class TestGenerator + * @SuppressWarnings(PHPMD) + */ +class DocGenerator +{ + const DEFAULT_OUTPUT_DIR = + PROJECT_ROOT . + DIRECTORY_SEPARATOR . + "dev" . + DIRECTORY_SEPARATOR . + "tests" . + DIRECTORY_SEPARATOR . + "docs"; + const DOC_NAME = "documentation.md"; + # This is the only place FILENAMES is defined as this string + const FILENAMES = "filenames"; + + /** + * DocGenerator constructor. + * + */ + public function __construct() + { + } + + /** + * This creates html documentation for objects passed in + * + * @param ActionGroupObject[]|TestObject[] $annotatedObjects + * @param string $outputDir + * @return void + * @throws TestFrameworkException + */ + public function createDocumentation($annotatedObjects, $outputDir, $clean) + { + if (empty($outputDir)) { + $fullPath = self::DEFAULT_OUTPUT_DIR . DIRECTORY_SEPARATOR; + } else { + $fullPath = $outputDir . DIRECTORY_SEPARATOR; + } + $filePath = $fullPath . self::DOC_NAME; + + if (!file_exists($fullPath)) { + mkdir($fullPath, 0755, true); + } + if (file_exists($filePath) and !$clean) { + throw new TestFrameworkException( + "$filePath already exists, please add --clean if you want to overwrite it." + ); + } + $pageGroups = []; + + foreach ($annotatedObjects as $name => $object) { + $annotations = $object->getAnnotations(); + $filenames = explode(',', $object->getFilename()); + $arguments = $object->getArguments(); + + $info = [ + actionGroupObject::ACTION_GROUP_DESCRIPTION => $annotations[actionGroupObject::ACTION_GROUP_DESCRIPTION] + ?? 'NO_DESCRIPTION_SPECIFIED', + self::FILENAMES => $filenames, + ActionGroupObjectExtractor::ACTION_GROUP_ARGUMENTS => $arguments + ]; + $pageGroups = array_merge_recursive( + $pageGroups, + [$annotations[ActionGroupObject::ACTION_GROUP_PAGE] ?? 'NO_PAGE_SPECIFIED' => [$name => $info]] + ); + } + + ksort($pageGroups); + foreach ($pageGroups as $page => $groups) { + ksort($groups); + $pageGroups[$page] = $groups; + } + + $markdown = $this->transformToMarkdown($pageGroups); + + file_put_contents($filePath, $markdown); + } + + /** + * This creates html documentation for objects passed in + * + * @param array $annotationList + * @return string + */ + private function transformToMarkdown($annotationList) + { + $markdown = "#Action Group Information" . PHP_EOL; + $markdown .= "This documentation contains a list of all" . + " action groups on the pages on which they start" . + PHP_EOL . + PHP_EOL; + + $markdown .= "##List of Pages" . PHP_EOL; + foreach ($annotationList as $group => $objects) { + $markdown .= "- [ $group ](#$group)" . PHP_EOL; + } + $markdown .= "---" . PHP_EOL; + foreach ($annotationList as $group => $objects) { + $markdown .= "<a name=\"$group\"></a>" . PHP_EOL; + $markdown .= "##$group" . PHP_EOL . PHP_EOL; + foreach ($objects as $name => $annotations) { + $markdown .= "###$name" . PHP_EOL; + $markdown .= $annotations[actionGroupObject::ACTION_GROUP_DESCRIPTION] . PHP_EOL . PHP_EOL; + if (!empty($annotations[ActionGroupObjectExtractor::ACTION_GROUP_ARGUMENTS])) { + $markdown .= "Action Group Arguments:" . PHP_EOL . PHP_EOL; + $markdown .= "| Name | Type |" . PHP_EOL; + $markdown .= "| --- | --- |" . PHP_EOL; + foreach ($annotations[ActionGroupObjectExtractor::ACTION_GROUP_ARGUMENTS] as $argument) { + $argumentName = $argument->getName(); + $argumentType = $argument->getDataType(); + $markdown .= "| $argumentName | $argumentType |" . PHP_EOL; + } + $markdown .= PHP_EOL; + } + $markdown .= "Located in:" . PHP_EOL; + foreach ($annotations[self::FILENAMES] as $filename) { + $relativeFilename = str_replace(MAGENTO_BP . DIRECTORY_SEPARATOR, "", $filename); + $markdown .= PHP_EOL . "- $relativeFilename"; + } + $markdown .= PHP_EOL . "***" . PHP_EOL; + } + } + return $markdown; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index abc62eb20..07cb96b3a 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -38,6 +38,7 @@ class TestGenerator const HOOK_SCOPE = 'hook'; const SUITE_SCOPE = 'suite'; const PRESSKEY_ARRAY_ANCHOR_KEY = '987654321098765432109876543210'; + const PERSISTED_OBJECT_NOTATION_REGEX = '/\${1,2}[\w.\[\]]+\${1,2}/'; /** * Path to the export dir. @@ -1332,7 +1333,7 @@ private function resolveTestVariable($args, $actionOrigin) } $outputArg = $arg; // Math on $data.key$ and $$data.key$$ - preg_match_all('/\${1,2}[\w.\[\]]+\${1,2}/', $outputArg, $matches); + preg_match_all(self::PERSISTED_OBJECT_NOTATION_REGEX, $outputArg, $matches); $this->replaceMatchesIntoArg($matches[0], $outputArg); //trim "{$variable}" into $variable