diff --git a/dev/tests/verification/Resources/AssertTest.txt b/dev/tests/verification/Resources/AssertTest.txt index adc764975..ac579d01f 100644 --- a/dev/tests/verification/Resources/AssertTest.txt +++ b/dev/tests/verification/Resources/AssertTest.txt @@ -52,7 +52,9 @@ class AssertTestCest null ); $grabTextFrom1 = $I->grabTextFrom(".copyright>span"); + $I->comment(" custom asserts "); $I->assertArrayIsSorted(["1", "2", "3", "4", "5"], "asc"); + $I->comment(" asserts without variable replacement "); $I->comment("asserts without variable replacement"); $I->assertArrayHasKey("apple", ['orange' => 2, 'apple' => 1], "pass"); $I->assertArrayNotHasKey("kiwi", ['orange' => 2, 'apple' => 1], "pass"); @@ -88,6 +90,7 @@ class AssertTestCest $I->assertStringStartsNotWith("a", "banana", "pass"); $I->assertStringStartsWith("a", "apple", "pass"); $I->assertTrue(true, "pass"); + $I->comment(" asserts backward compatible "); $I->comment("asserts backward compatible"); $I->assertArrayHasKey("apple", ['orange' => 2, 'apple' => 1], "pass"); $I->assertArrayNotHasKey("kiwi", ['orange' => 2, 'apple' => 1], "pass"); @@ -130,15 +133,18 @@ class AssertTestCest $I->assertIsEmpty($text, "pass"); $I->assertNull($text, "pass"); $I->expectException(new MyException('exception msg'), function() {$this->doSomethingBad();}); + $I->comment(" string type that use created data "); $I->comment("string type that use created data"); $I->assertStringStartsWith("D", PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'lastname', 'test') . ", " . PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'firstname', 'test'), "fail"); $I->assertStringStartsNotWith("W", PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'firstname', 'test') . ", " . PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'lastname', 'test'), "pass"); $I->assertEquals(PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'lastname', 'test'), PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'lastname', 'test'), "pass"); + $I->comment(" array type that use created data "); $I->comment("array type that use created data"); $I->assertArraySubset([PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'lastname', 'test'), PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'firstname', 'test')], [PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'lastname', 'test'), PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'firstname', 'test'), "1"], "pass"); $I->assertArraySubset([PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'firstname', 'test'), PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'lastname', 'test')], [PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'firstname', 'test'), PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'lastname', 'test'), "1"], "pass"); $I->assertArrayHasKey("lastname", ['lastname' => PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'lastname', 'test'), 'firstname' => PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'firstname', 'test')], "pass"); $I->assertArrayHasKey("lastname", ['lastname' => PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'lastname', 'test'), 'firstname' => PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'firstname', 'test')], "pass"); + $I->comment(" this section can only be generated and cannot run "); $I->assertInstanceOf(User::class, $text, "pass"); $I->assertNotInstanceOf(User::class, 21, "pass"); $I->assertFileExists($text, "pass"); @@ -149,6 +155,7 @@ class AssertTestCest $I->fail("fail"); $I->fail(PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'firstname', 'test') . " " . PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'lastname', 'test')); $I->fail(PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'firstname', 'test') . " " . PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'lastname', 'test')); + $I->comment(" assertElementContainsAttribute examples "); $I->assertElementContainsAttribute("#username", "class", "admin__control-text"); $I->assertElementContainsAttribute("#username", "name", "login[username]"); $I->assertElementContainsAttribute("#username", "autofocus", "true"); @@ -157,6 +164,7 @@ class AssertTestCest $I->assertElementContainsAttribute(".admin__menu-overlay", "border", "0"); $I->assertElementContainsAttribute("#username", "value", PersistedObjectHandler::getInstance()->retrieveEntityField('createData2', 'firstname', 'test')); $I->assertElementContainsAttribute("#username", "value", PersistedObjectHandler::getInstance()->retrieveEntityField('createData1', 'firstname', 'test')); + $I->comment(" assert entity resolution "); $I->assertEquals("John", "Doe", "pass"); } } diff --git a/dev/tests/verification/Resources/CommentTest.txt b/dev/tests/verification/Resources/CommentTest.txt new file mode 100644 index 000000000..b7871049e --- /dev/null +++ b/dev/tests/verification/Resources/CommentTest.txt @@ -0,0 +1,83 @@ +comment("< > & \$abc \" abc ' /"); + $I->amGoingTo("create entity that has the stepKey: createDataHook"); + PersistedObjectHandler::getInstance()->createEntity( + "createDataHook", + "hook", + "ReplacementPerson", + [], + null + ); + $I->comment(""); + } + + /** + * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) + * @Stories({"MQE-1234"}) + * @Parameter(name = "AcceptanceTester", value="$I") + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function CommentTest(AcceptanceTester $I) + { + $I->amGoingTo("create entity that has the stepKey: createDataTest"); + PersistedObjectHandler::getInstance()->createEntity( + "createDataTest", + "test", + "DefaultPerson", + [], + null + ); + $I->comment("comment("abc&"); + $I->comment(">abc"); + $I->comment("Entering Action Group commentActionGroup (commentInTest4)"); + $I->comment("xml comment in Action Group before"); + $I->comment("commentInActionGroup1"); + $I->wait(1); + $I->comment("commentInActionGroup2"); + $I->comment("xml comment in Action Group inside"); + $I->wait(1); + $I->comment("xml comment in Action Group after"); + $I->comment("Exiting Action Group commentActionGroup (commentInTest4)"); + $I->comment("< > & \$abc \" abc ' /"); + $I->comment(PersistedObjectHandler::getInstance()->retrieveEntityField('createDataHook', 'firstname', 'test')); + $I->comment(PersistedObjectHandler::getInstance()->retrieveEntityField('createDataTest', 'firstname', 'test')); + $I->comment("John" . msq("UniquePerson")); + $I->comment("< > & \$abc \" abc ' /"); + $I->comment(""); + $I->comment(""); + $I->skipReadinessCheck(true); + $I->comment("skipReadiness"); + $I->skipReadinessCheck(false); + } +} diff --git a/dev/tests/verification/TestModule/ActionGroup/CommentInActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/CommentInActionGroup.xml new file mode 100644 index 000000000..07979d96e --- /dev/null +++ b/dev/tests/verification/TestModule/ActionGroup/CommentInActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/dev/tests/verification/TestModule/Test/CommentTest.xml b/dev/tests/verification/TestModule/Test/CommentTest.xml new file mode 100644 index 000000000..b7ec81580 --- /dev/null +++ b/dev/tests/verification/TestModule/Test/CommentTest.xml @@ -0,0 +1,38 @@ + + + + + + + + + <group value="functional"/> + <features value="CommentTest"/> + <stories value="MQE-1234"/> + </annotations> + <before> + <!--< > & $abc " abc ' <click stepKey="click" userInput="{{UniquePerson.firstname}}" selector="#id">/--> + <createData entity="ReplacementPerson" stepKey="createDataHook"/> + <comment userInput="<abc>" stepKey="commentInHook"/> + </before> + <createData entity="DefaultPerson" stepKey="createDataTest"/> + <comment userInput="<abc" stepKey="commentInTest1"/> + <comment userInput="abc&" stepKey="commentInTest2"/> + <comment userInput=">abc" stepKey="commentInTest3"/> + <actionGroup ref="commentActionGroup" stepKey="commentInTest4"/> + <!--< > & $abc " abc ' <click stepKey="click" userInput="$$createDataHook.firstname$$" selector="#id">/--> + <comment userInput="$$createDataHook.firstname$$" stepKey="commentInTest5"/> + <comment userInput="$createDataTest.firstname$" stepKey="commentInTest6"/> + <comment userInput="{{UniquePerson.firstname}}" stepKey="commentInTest8"/> + <!--< > & $abc " abc ' <click stepKey="click" userInput="$createDataTest.firstname$" selector="#id">/--> + <comment stepKey="commentInTest9" userInput="{{emptyData.noData}}"/> + <comment stepKey="commentInTest10" userInput="{{emptyData.definitelyNoData}}"/> + <comment stepKey="commentInTest11" userInput="skipReadiness" skipReadiness="true"/> + </test> +</tests> \ No newline at end of file diff --git a/dev/tests/verification/Tests/CommentGenerationTest.php b/dev/tests/verification/Tests/CommentGenerationTest.php new file mode 100644 index 000000000..60bcea45d --- /dev/null +++ b/dev/tests/verification/Tests/CommentGenerationTest.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use tests\util\MftfTestCase; + +class CommentGenerationTest extends MftfTestCase +{ + /** + * Tests flat generation of a hardcoded test file with no external references. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testCommentGeneration() + { + $this->generateAndCompareTest('CommentTest'); + } +} diff --git a/docs/test.md b/docs/test.md index 462cd138e..0be6675d3 100644 --- a/docs/test.md +++ b/docs/test.md @@ -127,6 +127,12 @@ Attribute|Type|Use See [Action groups][action group] for more information. +### XML Comments + +xml comments in `/tests/test`, but other than in `/tests/test/annotations`, will be parse and converted in place into `comment` actions. + +xml comments in `/actionGroups/actionGroup`, but other than in `/actionGroups/actionGroup/arguments`, will be parse and converted in place into `comment` action. + <!-- Link definitions --> [`<actionGroup>`]: #actiongroup-tag diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php b/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php index dd72264b9..d22670582 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php +++ b/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php @@ -18,6 +18,8 @@ class Flat implements ConverterInterface const REMOVE_KEY_ATTRIBUTE = 'keyForRemoval'; const EXTENDS_ATTRIBUTE = 'extends'; const TEST_HOOKS = ['before', 'after']; + const COMMENT_ACTION = 'comment'; + const XML_COMMENT_ACTION = 'xmlComment'; /** * Array node configuration. @@ -119,6 +121,23 @@ public function convertXml(\DOMNode $source, $basePath = '') } else { $value[$nodeName] = $nodeData; } + // A new "xmlComment" node is inserted when xml comment is found in non-annotation portion of tests + // or in non-arguments portion of action groups + } elseif ($node->nodeType == XML_COMMENT_NODE) { + if ((strpos($basePath, '/tests/test') !== false + && strpos($basePath, '/tests/test/annotations') === false) + || (strpos($basePath, '/actionGroups/actionGroup') !== false + && strpos($basePath, '/actionGroups/actionGroup/arguments') === false)) { + $nodePath = $basePath . '/' . self::COMMENT_ACTION; + $arrayKeyAttribute = $this->arrayNodeConfig->getAssocArrayKeyAttribute($nodePath); + $arrayKeyValue = self::COMMENT_ACTION . uniqid(); + $nodeData = [ + "nodeName" => self::XML_COMMENT_ACTION, + "userInput" => $node->nodeValue, + $arrayKeyAttribute => $arrayKeyValue, + ]; + $value[$arrayKeyValue] = $nodeData; + } } elseif ($node->nodeType == XML_CDATA_SECTION_NODE || ($node->nodeType == XML_TEXT_NODE && trim($node->nodeValue) != '') ) { diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php index 32e314be9..ee16a9496 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -72,6 +72,7 @@ class ActionObject const DEFAULT_WAIT_TIMEOUT = 10; const ACTION_ATTRIBUTE_USERINPUT = 'userInput'; const ACTION_TYPE_COMMENT = 'comment'; + const ACTION_TYPE_XML_COMMENT = 'xmlComment'; /** * The unique identifier for the action @@ -191,7 +192,7 @@ public function getType() /** * Getter for actionOrigin * - * @return string + * @return array */ public function getActionOrigin() { @@ -267,7 +268,7 @@ public function setTimeout($timeout) */ public function resolveReferences() { - if (empty($this->resolvedCustomAttributes)) { + if (empty($this->resolvedCustomAttributes) && $this->getType() != self::ACTION_TYPE_XML_COMMENT) { $this->trimAssertionAttributes(); $this->resolveSelectorReferenceAndTimeout(); $this->resolveUrlReference(); diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index 07cb96b3a..b8a3c5ab0 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -542,19 +542,23 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $sortOrder = $customActionAttributes['sortOrder']; } - if (isset($customActionAttributes['userInput']) && isset($customActionAttributes['url'])) { - $input = $this->addUniquenessFunctionCall($customActionAttributes['userInput']); - $url = $this->addUniquenessFunctionCall($customActionAttributes['url']); + if ($actionObject->getType() != ActionObject::ACTION_TYPE_XML_COMMENT) { + if (isset($customActionAttributes['userInput']) && isset($customActionAttributes['url'])) { + $input = $this->addUniquenessFunctionCall($customActionAttributes['userInput']); + $url = $this->addUniquenessFunctionCall($customActionAttributes['url']); + } elseif (isset($customActionAttributes['userInput'])) { + $input = $this->addUniquenessFunctionCall($customActionAttributes['userInput']); + } elseif (isset($customActionAttributes['url'])) { + $input = $this->addUniquenessFunctionCall($customActionAttributes['url']); + $url = $this->addUniquenessFunctionCall($customActionAttributes['url']); + } elseif (isset($customActionAttributes['expectedValue'])) { + //For old Assert backwards Compatibility, remove when deprecating + $assertExpected = $this->addUniquenessFunctionCall($customActionAttributes['expectedValue']); + } elseif (isset($customActionAttributes['regex'])) { + $input = $this->addUniquenessFunctionCall($customActionAttributes['regex']); + } } elseif (isset($customActionAttributes['userInput'])) { - $input = $this->addUniquenessFunctionCall($customActionAttributes['userInput']); - } elseif (isset($customActionAttributes['url'])) { - $input = $this->addUniquenessFunctionCall($customActionAttributes['url']); - $url = $this->addUniquenessFunctionCall($customActionAttributes['url']); - } elseif (isset($customActionAttributes['expectedValue'])) { - //For old Assert backwards Compatibility, remove when deprecating - $assertExpected = $this->addUniquenessFunctionCall($customActionAttributes['expectedValue']); - } elseif (isset($customActionAttributes['regex'])) { - $input = $this->addUniquenessFunctionCall($customActionAttributes['regex']); + $input = $this->escapeStringInDoubleQuotes($customActionAttributes['userInput']); } if (isset($customActionAttributes['date']) && isset($customActionAttributes['format'])) { @@ -1286,6 +1290,13 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato case "skipReadinessCheck": $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $customActionAttributes['state']); break; + case ActionObject::ACTION_TYPE_XML_COMMENT: + $testSteps .= sprintf( + "\t\t$%s->comment(%s);\n", + $actor, + $input + ); + break; default: $testSteps .= $this->wrapFunctionCall( $actor, @@ -1768,15 +1779,13 @@ private function addDollarSign($input) * Wrap parameters into a function call. * * @param string $actor - * @param actionObject $action - * @param string $scope + * @param ActionObject $action * @param array ...$args * @return string * @throws \Exception */ private function wrapFunctionCall($actor, $action, ...$args) { - $isFirst = true; $output = sprintf("\t\t$%s->%s(", $actor, $action->getType()); for ($i = 0; $i < count($args); $i++) { if (null === $args[$i]) { @@ -1800,15 +1809,13 @@ private function wrapFunctionCall($actor, $action, ...$args) * * @param string $returnVariable * @param string $actor - * @param string $action - * @param string $scope + * @param ActionObject $action * @param array ...$args * @return string * @throws \Exception */ private function wrapFunctionCallWithReturnValue($returnVariable, $actor, $action, ...$args) { - $isFirst = true; $output = sprintf("\t\t$%s = $%s->%s(", $returnVariable, $actor, $action->getType()); for ($i = 0; $i < count($args); $i++) { if (null === $args[$i]) { @@ -1880,6 +1887,24 @@ private function resolveAllRuntimeReferences($args) return $argResult; } + /** + * Escape input string within a pair of double quotes + * + * @param string $input + * @return string + */ + private function escapeStringInDoubleQuotes($input) + { + if ($input == null) { + return ''; + } + // Replace " with \" + $input = str_replace('"', '\"', $input); + // Replace $ with \$ + $input = str_replace('$', '\$', $input); + return sprintf('"%s"', $input); + } + /** * Validates parameter array format, making sure user has enclosed string with square brackets. *