diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Api/ApiExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Api/ApiExecutor.php index ee0f6eb81..2cad2bf16 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Api/ApiExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Api/ApiExecutor.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\FunctionalTestingFramework\DataGenerator\Api; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; @@ -12,6 +13,7 @@ use Magento\FunctionalTestingFramework\DataGenerator\Objects\JsonElement; use Magento\FunctionalTestingFramework\DataGenerator\Util\JsonObjectExtractor; use Magento\FunctionalTestingFramework\Util\ApiClientUtil; +use Magento\Setup\Exception; /** * Class ApiExecutor @@ -19,6 +21,7 @@ class ApiExecutor { const PRIMITIVE_TYPES = ['string', 'boolean', 'integer', 'double', 'array']; + const EXCEPTION_REQUIRED_DATA = "%s of key \" %s\" in \"%s\" is required by metadata, but was not provided."; /** * Describes the operation for the executor ('create','update','delete') @@ -149,6 +152,7 @@ private function getAuthorizationHeader($authUrl) * @param EntityDataObject $entityObject * @param array $jsonArrayMetadata * @return array + * @throws \Exception */ private function convertJsonArray($entityObject, $jsonArrayMetadata) { @@ -157,8 +161,10 @@ private function convertJsonArray($entityObject, $jsonArrayMetadata) foreach ($jsonArrayMetadata as $jsonElement) { if ($jsonElement->getType() == JsonObjectExtractor::JSON_OBJECT_OBJ_NAME) { + $entityObj = $this->resolveJsonObjectAndEntityData($entityObject, $jsonElement->getValue()); $jsonArray[$jsonElement->getValue()] = - $this->convertJsonArray($entityObject, $jsonElement->getNestedMetadata()); + $this->convertJsonArray($entityObj, $jsonElement->getNestedMetadata()); + continue; } $jsonElementType = $jsonElement->getValue(); @@ -169,20 +175,40 @@ private function convertJsonArray($entityObject, $jsonArrayMetadata) EntityDataObject::CEST_UNIQUE_VALUE ); - if (array_key_exists($jsonElement->getKey(), $entityObject->getUniquenessData())) { - $uniqueData = $entityObject->getUniquenessDataByName($jsonElement->getKey()); - if ($uniqueData === 'suffix') { - $elementData .= (string)self::getSequence($entityObject->getName()); - } else { - $elementData = (string)self::getSequence($entityObject->getName()) - . $elementData; + // If data was defined at all, attempt to put it into JSON body + // If data was not defined, and element is required, throw exception + // If no data is defined, don't input defaults per primitive into JSON for the data + if ($elementData != null) { + if (array_key_exists($jsonElement->getKey(), $entityObject->getUniquenessData())) { + $uniqueData = $entityObject->getUniquenessDataByName($jsonElement->getKey()); + if ($uniqueData === 'suffix') { + $elementData .= (string)self::getSequence($entityObject->getName()); + } else { + $elementData = (string)self::getSequence($entityObject->getName()) . $elementData; + } } + $jsonArray[$jsonElement->getKey()] = $this->castValue($jsonElementType, $elementData); + + } elseif ($jsonElement->getRequired()) { + throw new \Exception(sprintf( + ApiExecutor::EXCEPTION_REQUIRED_DATA, + $jsonElement->getType(), + $jsonElement->getKey(), + $this->entityObject->getName() + )); } - - $jsonArray[$jsonElement->getKey()] = $this->castValue($jsonElementType, $elementData); } else { $entityNamesOfType = $entityObject->getLinkedEntitiesOfType($jsonElementType); + // If an element is required by metadata, but was not provided in the entity, throw an exception + if ($jsonElement->getRequired() && $entityNamesOfType == null) { + throw new \Exception(sprintf( + ApiExecutor::EXCEPTION_REQUIRED_DATA, + $jsonElement->getType(), + $jsonElement->getKey(), + $this->entityObject->getName() + )); + } foreach ($entityNamesOfType as $entityName) { $jsonDataSubArray = $this->resolveNonPrimitiveElement($entityName, $jsonElement); @@ -198,6 +224,25 @@ private function convertJsonArray($entityObject, $jsonArrayMetadata) return $jsonArray; } + /** + * This function does a comparison of the entity object being matched to the json element. If there is a mismatch in + * type we attempt to use a nested entity, if the entities are properly matched, we simply return the object. + * + * @param EntityDataObject $entityObject + * @param string $jsonElementValue + * @return EntityDataObject|null + */ + private function resolveJsonObjectAndEntityData($entityObject, $jsonElementValue) + { + if ($jsonElementValue != $entityObject->getType()) { + // if we have a mismatch attempt to retrieve linked data and return just the first linkage + $linkName = $entityObject->getLinkedEntitiesOfType($jsonElementValue)[0]; + return DataObjectHandler::getInstance()->getObject($linkName); + } + + return $entityObject; + } + /** * Resolves JsonObjects and pre-defined metadata (in other operation.xml file) referenced by the json metadata * @@ -209,8 +254,10 @@ private function resolveNonPrimitiveElement($entityName, $jsonElement) { $linkedEntityObj = $this->resolveLinkedEntityObject($entityName); + // in array case if (!empty($jsonElement->getNestedJsonElement($jsonElement->getValue())) - && $jsonElement->getType() == 'array') { + && $jsonElement->getType() == 'array' + ) { $jsonSubArray = $this->convertJsonArray( $linkedEntityObj, [$jsonElement->getNestedJsonElement($jsonElement->getValue())] @@ -285,6 +332,7 @@ private static function getSequence($entityName) } // @codingStandardsIgnoreStart + /** * This function takes a string value and its corresponding type and returns the string cast * into its the type passed. @@ -304,6 +352,9 @@ private function castValue($type, $value) $newVal = (integer)$value; break; case 'boolean': + if (strtolower($newVal) === 'false') { + return false; + } $newVal = (boolean)$value; break; case 'double': diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/JsonDefinitionObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/JsonDefinitionObjectHandler.php index 165962409..9243ff46e 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/JsonDefinitionObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/JsonDefinitionObjectHandler.php @@ -37,6 +37,7 @@ class JsonDefinitionObjectHandler implements ObjectHandlerInterface const ENTITY_OPERATION_OBJECT_KEY = 'key'; const ENTITY_OPERATION_OBJECT_VALUE = 'value'; const ENTITY_OPERATION_JSON_OBJECT = 'jsonObject'; + const ENTITY_OPERATION_REQUIRED = 'required'; /** * Singleton Instance of class @@ -164,7 +165,8 @@ private function initJsonDefinitions() $jsonMetadata[] = new JsonElement( $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY], $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE], - JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY + JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY, + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_REQUIRED] ?? null ); } } @@ -192,6 +194,7 @@ private function initJsonDefinitions() $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_KEY], $value, $type, + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_REQUIRED] ?? null, $jsonSubMetadata ); } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonElement.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonElement.php index 03f0192cc..a2af57341 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonElement.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/JsonElement.php @@ -43,20 +43,32 @@ class JsonElement */ private $nestedMetadata = []; + /** + * Json required attribute, used to determine if values need to be cast before insertion. + * @var bool + */ + private $required; + /** * JsonElement constructor. * @param string $key * @param string $value * @param string $type + * @param bool $required * @param array $nestedElements - * @param array $nestedMetadata + * @param null|array $nestedMetadata */ - public function __construct($key, $value, $type, $nestedElements = [], $nestedMetadata = null) + public function __construct($key, $value, $type, $required, $nestedElements = [], $nestedMetadata = null) { $this->key = $key; $this->value = $value; $this->type = $type; $this->nestedElements = $nestedElements; + if ($required) { + $this->required = true; + } else { + $this->required = false; + } $this->nestedMetadata = $nestedMetadata; } @@ -90,6 +102,16 @@ public function getType() return $this->type; } + /** + * Getter for required attribute + * + * @return bool + */ + public function getRequired() + { + return $this->required; + } + /** * Returns the nested json element based on the type of entity passed * diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/JsonObjectExtractor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/JsonObjectExtractor.php index 6f30192b9..f5c80065a 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/JsonObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/JsonObjectExtractor.php @@ -58,7 +58,7 @@ public function extractJsonObject($jsonObjectArray) if (array_key_exists(JsonObjectExtractor::JSON_OBJECT_OBJ_NAME, $jsonObjectArray)) { foreach ($jsonObjectArray[JsonObjectExtractor::JSON_OBJECT_OBJ_NAME] as $jsonObject) { $nestedJsonElement = $this->extractJsonObject($jsonObject); - $nestedJsonElements[$nestedJsonElement->getKey()] = $nestedJsonElement; + $jsonMetadata[] = $nestedJsonElement; } } @@ -71,6 +71,7 @@ public function extractJsonObject($jsonObjectArray) $jsonDefKey, $dataType, JsonObjectExtractor::JSON_OBJECT_OBJ_NAME, + $jsonObjectArray[JsonDefinitionObjectHandler::ENTITY_OPERATION_REQUIRED] ?? null, $nestedJsonElements, $jsonMetadata ); @@ -89,7 +90,8 @@ private function extractJsonEntries(&$jsonMetadata, $jsonEntryArray) $jsonMetadata[] = new JsonElement( $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY], $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE], - JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY + JsonDefinitionObjectHandler::ENTITY_OPERATION_ENTRY, + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_REQUIRED] ?? null ); } } @@ -122,6 +124,7 @@ private function extractJsonArrays(&$jsonArrayData, $jsonArrayArray) $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_KEY], $jsonElementValue, JsonDefinitionObjectHandler::ENTITY_OPERATION_ARRAY, + $jsonEntryType[JsonDefinitionObjectHandler::ENTITY_OPERATION_REQUIRED] ?? null, $nestedJsonElements ); } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd index 52e144f22..2c4fcabc7 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd @@ -31,12 +31,14 @@ + + @@ -47,6 +49,7 @@ + diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/TestReferenceException.php b/src/Magento/FunctionalTestingFramework/Exceptions/TestReferenceException.php new file mode 100644 index 000000000..1fd2e4f67 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Exceptions/TestReferenceException.php @@ -0,0 +1,22 @@ +_findElements($locator); - } - /** * Login Magento Admin with given username and password. * @@ -195,6 +184,9 @@ public function waitForPageLoad($timeout = 15) { $this->waitForJS('return document.readyState == "complete"', $timeout); $this->waitForAjaxLoad($timeout); + $this->waitForElementNotVisible('.loading-mask', 30); + $this->waitForElementNotVisible('.admin_data-grid-loading-mask', 30); + $this->waitForElementNotVisible('.admin__form-loading-mask', 30); } /** @@ -282,6 +274,32 @@ public function scrollToTopOfPage() $this->executeJS('window.scrollTo(0,0);'); } + /** + * Conditional click for an area that should be visible + * + * @param string $selector + * @param string dependentSelector + * @param bool $visible + */ + public function conditionalClick($selector, $dependentSelector, $visible) + { + $el = $this->_findElements($dependentSelector); + if (sizeof($el) > 1) { + throw new \Exception("more than one element matches selector " . $selector); + } + + $clickCondition = null; + if ($visible) { + $clickCondition = !empty($el) && $el[0]->isDisplayed(); + } else { + $clickCondition = empty($el) || !$el[0]->isDisplayed(); + } + + if ($clickCondition) { + $this->click($selector); + } + } + /** * Override for _failed method in Codeception method. Adds png and html attachments to allure report * following parent execution of test failure processing. diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php index f9376f9d4..121d3e6ca 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/SectionObject.php @@ -56,14 +56,28 @@ public function getElements() return $this->elements; } + /** + * Checks to see if this section contains any element by the name of elementName + * @param string $elementName + * @return bool + */ + public function hasElement($elementName) + { + return array_key_exists($elementName, $this->elements); + } + /** * Given the name of an element, returns the element object * * @param string $elementName - * @return ElementObject + * @return ElementObject | null */ public function getElement($elementName) { - return $this->elements[$elementName]; + if ($this->hasElement($elementName)) { + return $this->elements[$elementName]; + } + + return null; } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php index df30cc213..e51117e36 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php @@ -65,7 +65,7 @@ public function getSteps($arguments) $args = array_merge($args, $arguments); } - return $mergeUtil->mergeStepsAndInsertWaits($this->getResolvedActionsWithArgs($args)); + return $mergeUtil->resolveActionSteps($this->getResolvedActionsWithArgs($args), true); } /** diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php index de3e58e5c..cb58c361b 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -12,6 +12,7 @@ use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; use Magento\FunctionalTestingFramework\Page\Handlers\PageObjectHandler; use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; /** * Class ActionObject @@ -19,6 +20,7 @@ class ActionObject { const DATA_ENABLED_ATTRIBUTES = ["userInput", "parameterArray"]; + const SELECTOR_ENABLED_ATTRIBUTES = ['selector', 'dependentSelector']; const MERGE_ACTION_ORDER_AFTER = 'after'; const ACTION_ATTRIBUTE_URL = 'url'; const ACTION_ATTRIBUTE_SELECTOR = 'selector'; @@ -188,15 +190,20 @@ public function resolveReferences() */ private function resolveSelectorReferenceAndTimeout() { - if (!array_key_exists(ActionObject::ACTION_ATTRIBUTE_SELECTOR, $this->actionAttributes)) { + $actionAttributeKeys = array_keys($this->actionAttributes); + $relevantSelectorAttributes = array_intersect($actionAttributeKeys, ActionObject::SELECTOR_ENABLED_ATTRIBUTES); + + if (empty($relevantSelectorAttributes)) { return; } - $selector = $this->actionAttributes[ActionObject::ACTION_ATTRIBUTE_SELECTOR]; + foreach ($relevantSelectorAttributes as $selectorAttribute) { + $selector = $this->actionAttributes[$selectorAttribute]; - $replacement = $this->findAndReplaceReferences(SectionObjectHandler::getInstance(), $selector); - if ($replacement) { - $this->resolvedCustomAttributes[ActionObject::ACTION_ATTRIBUTE_SELECTOR] = $replacement; + $replacement = $this->findAndReplaceReferences(SectionObjectHandler::getInstance(), $selector); + if ($replacement) { + $this->resolvedCustomAttributes[$selectorAttribute] = $replacement; + } } } @@ -321,8 +328,12 @@ private function findAndReplaceReferences($objectHandler, $inputString) break; case SectionObject::class: list(,$objField) = $this->stripAndSplitReference($match); + if ($obj->getElement($objField) == null) { + throw new TestReferenceException("Could not resolve entity reference " . $inputString); + } $parameterized = $obj->getElement($objField)->isParameterized(); $replacement = $obj->getElement($objField)->getLocator(); + $this->timeout = $obj->getElement($objField)->getTimeout(); break; case (get_class($obj) == EntityDataObject::class): list(,$objField) = $this->stripAndSplitReference($match); @@ -346,7 +357,7 @@ private function findAndReplaceReferences($objectHandler, $inputString) if ($replacement == null && get_class($objectHandler) != DataObjectHandler::class) { return $this->findAndReplaceReferences(DataObjectHandler::getInstance(), $outputString); } elseif ($replacement == null) { - throw new \Exception("Could not resolve entity reference " . $inputString); + throw new TestReferenceException("Could not resolve entity reference " . $inputString); } //If Page or Section's Element is has parameterized = true attribute, attempt to do parameter replacement. @@ -372,12 +383,12 @@ private function matchParameterReferences($reference, $parameters) { preg_match_all('/{{[\w.]+}}/', $reference, $varMatches); if (count($varMatches[0]) > count($parameters)) { - throw new \Exception( + throw new TestReferenceException( "Parameter Resolution Failed: Not enough parameters given for reference " . $reference . ". Parameters Given: " . implode(",", $parameters) ); } elseif (count($varMatches[0]) < count($parameters)) { - throw new \Exception( + throw new TestReferenceException( "Parameter Resolution Failed: Too many parameters given for reference " . $reference . ". Parameters Given: " . implode(",", $parameters) ); diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/CestHookObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/CestHookObject.php index 3837aaf69..7d62b32ec 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/CestHookObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/CestHookObject.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Test\Objects; +use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; + /** * Class CestHookObject */ @@ -61,7 +63,8 @@ public function getType() */ public function getActions() { - return $this->actions; + $mergeUtil = new ActionMergeUtil(); + return $mergeUtil->resolveActionSteps($this->actions); } /** diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php index d4b79075f..97865adb1 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php @@ -99,34 +99,6 @@ public function getCustomData() public function getOrderedActions() { $mergeUtil = new ActionMergeUtil(); - $mergedSteps = $mergeUtil->mergeStepsAndInsertWaits($this->parsedSteps); - return $this->extractActionGroups($mergedSteps); - } - - /** - * Method to insert action group references into step flow - * - * @param array $mergedSteps - * @return array - */ - private function extractActionGroups($mergedSteps) - { - $newOrderedList = []; - - foreach ($mergedSteps as $key => $mergedStep) { - /**@var ActionObject $mergedStep**/ - if ($mergedStep->getType() == ActionObjectExtractor::ACTION_GROUP_TAG) { - $actionGroup = ActionGroupObjectHandler::getInstance()->getObject( - $mergedStep->getCustomActionAttributes()[ActionObjectExtractor::ACTION_GROUP_REF] - ); - $args = $mergedStep->getCustomActionAttributes()[ActionObjectExtractor::ACTION_GROUP_ARGUMENTS] ?? null; - $actionsToMerge = $actionGroup->getSteps($args); - $newOrderedList = $newOrderedList + $actionsToMerge; - } else { - $newOrderedList[$key] = $mergedStep; - } - } - - return $newOrderedList; + return $mergeUtil->resolveActionSteps($this->parsedSteps); } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php index 92ee032b3..133ace223 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Test\Util; +use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; /** @@ -44,14 +45,46 @@ public function __construct() * Method to execute merge of steps and insert wait steps. * * @param array $parsedSteps + * @param bool $skipActionGroupResolution * @return array */ - public function mergeStepsAndInsertWaits($parsedSteps) + public function resolveActionSteps($parsedSteps, $skipActionGroupResolution = false) { $this->mergeActions($parsedSteps); $this->insertWaits(); - return $this->orderedSteps; + if ($skipActionGroupResolution) { + return $this->orderedSteps; + } + + return $this->resolveActionGroups($this->orderedSteps); + } + + /** + * Method to resolve action group references and insert relevant actions into step flow + * + * @param array $mergedSteps + * @return array + */ + private function resolveActionGroups($mergedSteps) + { + $newOrderedList = []; + + foreach ($mergedSteps as $key => $mergedStep) { + /**@var ActionObject $mergedStep**/ + if ($mergedStep->getType() == ActionObjectExtractor::ACTION_GROUP_TAG) { + $actionGroup = ActionGroupObjectHandler::getInstance()->getObject( + $mergedStep->getCustomActionAttributes()[ActionObjectExtractor::ACTION_GROUP_REF] + ); + $args = $mergedStep->getCustomActionAttributes()[ActionObjectExtractor::ACTION_GROUP_ARGUMENTS] ?? null; + $actionsToMerge = $actionGroup->getSteps($args); + $newOrderedList = $newOrderedList + $actionsToMerge; + } else { + $newOrderedList[$key] = $mergedStep; + } + } + + return $newOrderedList; } /** diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/sampleCest.xml b/src/Magento/FunctionalTestingFramework/Test/etc/sampleCest.xml index 8bcec49ae..2ae9e1260 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/sampleCest.xml +++ b/src/Magento/FunctionalTestingFramework/Test/etc/sampleCest.xml @@ -21,6 +21,7 @@ + diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd index 945f2e133..94d6d5a7a 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd @@ -84,6 +84,7 @@ + @@ -352,6 +353,19 @@ + + + + + + + + + + + + + diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index 6799e4bb5..2798329bc 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -6,13 +6,25 @@ namespace Magento\FunctionalTestingFramework\Util; +use FilesystemIterator; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Test\Handlers\CestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\CestObject; +use RecursiveDirectoryIterator; class TestGenerator { + + /** + * Path to the export dir. + * + * @var string + */ + private $exportDirectory; + /** * Test generator. * @@ -22,10 +34,50 @@ class TestGenerator /** * TestGenerator constructor. + * @param string $exportDir */ - private function __construct() + private function __construct($exportDir) { // private constructor for singleton + $this->exportDirectory = $exportDir; + } + + /** + * Method used to clean export dir if needed and create new empty export dir. + * + * @return void + */ + private function setupExportDir() + { + if (file_exists($this->exportDirectory)) { + $this->rmDirRecursive($this->exportDirectory); + } + + mkdir($this->exportDirectory, 0777, true); + } + + /** + * Takes a directory path and recursively deletes all files and folders. + * + * @param string $directory + * @return void + */ + private function rmdirRecursive($directory) + { + $it = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS); + + while ($it->valid()) { + $path = $directory . DIRECTORY_SEPARATOR . $it->getFilename(); + if ($it->isDir()) { + $this->rmDirRecursive($path); + } else { + unlink($path); + } + + $it->next(); + } + + rmdir($directory); } /** @@ -36,7 +88,7 @@ private function __construct() public static function getInstance() { if (!self::$testGenerator) { - self::$testGenerator = new TestGenerator(); + self::$testGenerator = new TestGenerator(TESTS_MODULE_PATH . DIRECTORY_SEPARATOR . "_generated"); } return self::$testGenerator; @@ -63,13 +115,7 @@ private function loadAllCestObjects() */ private function createCestFile($cestPhp, $filename) { - $exportDirectory = TESTS_MODULE_PATH . "/_generated"; - $exportFilePath = sprintf("%s/%s.php", $exportDirectory, $filename); - - if (!is_dir($exportDirectory)) { - mkdir($exportDirectory, 0777, true); - } - + $exportFilePath = $this->exportDirectory . DIRECTORY_SEPARATOR . $filename . ".php"; $file = fopen($exportFilePath, 'w'); if (!$file) { @@ -88,6 +134,7 @@ private function createCestFile($cestPhp, $filename) */ public function createAllCestFiles() { + $this->setupExportDir(); $cestPhpArray = $this->assembleAllCestPhp(); foreach ($cestPhpArray as $cestPhpFile) { @@ -108,8 +155,12 @@ private function assembleCestPhp($cestObject) $classAnnotationsPhp = $this->generateClassAnnotationsPhp($cestObject->getAnnotations()); $className = $cestObject->getName(); $className = str_replace(' ', '', $className); - $hookPhp = $this->generateHooksPhp($cestObject->getHooks()); - $testsPhp = $this->generateTestsPhp($cestObject->getTests()); + try { + $hookPhp = $this->generateHooksPhp($cestObject->getHooks()); + $testsPhp = $this->generateTestsPhp($cestObject->getTests()); + } catch (TestReferenceException $e) { + throw new TestReferenceException($e->getMessage(). " in Cest \"" . $cestObject->getName() . "\""); + } $cestPhp = "loadAllCestObjects(); $cestPhpArray = []; + // create our manifest file here + $testManifest = new TestManifest($this->exportDirectory); + foreach ($cestObjects as $cest) { $name = $cest->getName(); $name = $string = str_replace(' ', '', $name); $php = $this->assembleCestPhp($cest); $cestPhpArray[] = [$name, $php]; + + //write to manifest here + $testManifest->recordCest($cest->getName(), $cest->getTests()); } return $cestPhpArray; @@ -293,6 +350,8 @@ private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) $value = null; $button = null; $parameter = null; + $dependentSelector = null; + $visible = null; if (isset($customActionAttributes['returnVariable'])) { $returnVariable = $customActionAttributes['returnVariable']; @@ -326,7 +385,7 @@ private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) foreach ($params as $param) { $paramsWithUniqueness[] = $this->addUniquenessFunctionCall($param); } - $parameterArray = '[' . implode(',', $paramsWithUniqueness) .']'; + $parameterArray = '[' . implode(',', $paramsWithUniqueness) . ']'; } if (isset($customActionAttributes['requiredAction'])) { @@ -336,15 +395,15 @@ private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) if (isset($customActionAttributes['selectorArray'])) { $selector = $customActionAttributes['selectorArray']; } elseif (isset($customActionAttributes['selector'])) { - $selector = $this->wrapWithSingleQuotes($customActionAttributes['selector']); + $selector = $this->wrapWithDoubleQuotes($customActionAttributes['selector']); } if (isset($customActionAttributes['selector1'])) { - $selector1 = $this->wrapWithSingleQuotes($customActionAttributes['selector1']); + $selector1 = $this->wrapWithDoubleQuotes($customActionAttributes['selector1']); } if (isset($customActionAttributes['selector2'])) { - $selector2 = $this->wrapWithSingleQuotes($customActionAttributes['selector2']); + $selector2 = $this->wrapWithDoubleQuotes($customActionAttributes['selector2']); } if (isset($customActionAttributes['x'])) { @@ -364,15 +423,15 @@ private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) } if (isset($customActionAttributes['locale'])) { - $locale = $this->wrapWithSingleQuotes($customActionAttributes['locale']); + $locale = $this->wrapWithDoubleQuotes($customActionAttributes['locale']); } if (isset($customActionAttributes['username'])) { - $username = $this->wrapWithSingleQuotes($customActionAttributes['username']); + $username = $this->wrapWithDoubleQuotes($customActionAttributes['username']); } if (isset($customActionAttributes['password'])) { - $password = $this->wrapWithSingleQuotes($customActionAttributes['password']); + $password = $this->wrapWithDoubleQuotes($customActionAttributes['password']); } if (isset($customActionAttributes['width'])) { @@ -384,15 +443,23 @@ private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) } if (isset($customActionAttributes['value'])) { - $value = $this->wrapWithSingleQuotes($customActionAttributes['value']); + $value = $this->wrapWithDoubleQuotes($customActionAttributes['value']); } if (isset($customActionAttributes['button'])) { - $button = $this->wrapWithSingleQuotes($customActionAttributes['button']); + $button = $this->wrapWithDoubleQuotes($customActionAttributes['button']); } if (isset($customActionAttributes['parameter'])) { - $parameter = $this->wrapWithSingleQuotes($customActionAttributes['parameter']); + $parameter = $this->wrapWithDoubleQuotes($customActionAttributes['parameter']); + } + + if (isset($customActionAttributes['dependentSelector'])) { + $dependentSelector = $this->wrapWithDoubleQuotes($customActionAttributes['dependentSelector']); + } + + if (isset($customActionAttributes['visible'])) { + $visible = $customActionAttributes['visible']; } switch ($actionName) { @@ -583,7 +650,7 @@ private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) $testSteps .= $this->wrapFunctionCall($actor, $actionName, $function); break; case "executeJS": - $testSteps .= $this->wrapFunctionCall($actor, $actionName, $this->wrapWithSingleQuotes($function)); + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $this->wrapWithDoubleQuotes($function)); break; case "performOn": case "waitForElementChange": @@ -593,7 +660,7 @@ private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) $testSteps .= $this->wrapFunctionCall( $actor, $actionName, - $this->wrapWithSingleQuotes($function), + $this->wrapWithDoubleQuotes($function), $time ); break; @@ -702,6 +769,9 @@ private function generateStepsPhp($stepsObject, $stepsData, $hookObject = false) // TODO: Need to fix xml parser to allow parsing html. $testSteps .= $this->wrapFunctionCall($actor, $actionName, $html); break; + case "conditionalClick": + $testSteps .= $this->wrapFunctionCall($actor, $actionName, $selector, $dependentSelector, $visible); + break; default: if ($returnVariable) { $testSteps .= $this->wrapFunctionCallWithReturnValue( @@ -815,7 +885,15 @@ private function generateHooksPhp($hookObjects) } } - $steps = $this->generateStepsPhp($hookObject->getActions(), $hookObject->getCustomData(), $createData); + try { + $steps = $this->generateStepsPhp( + $hookObject->getActions(), + $hookObject->getCustomData(), + $createData + ); + } catch (TestReferenceException $e) { + throw new TestReferenceException($e->getMessage() . " in Element \"" . $type . "\""); + } if ($type == "after") { $hooks .= sprintf("\tpublic function _after(%s)\n", $dependencies); @@ -938,7 +1016,11 @@ private function generateTestsPhp($testsObject) $testName = str_replace(' ', '', $testName); $testAnnotations = $this->generateTestAnnotationsPhp($test->getAnnotations()); $dependencies = 'AcceptanceTester $I'; - $steps = $this->generateStepsPhp($test->getOrderedActions(), $test->getCustomData()); + try { + $steps = $this->generateStepsPhp($test->getOrderedActions(), $test->getCustomData()); + } catch (TestReferenceException $e) { + throw new TestReferenceException($e->getMessage() . " in Test \"" . $test->getName() . "\""); + } $testPhp .= $testAnnotations; $testPhp .= sprintf("\tpublic function %s(%s)\n", $testName, $dependencies); @@ -964,38 +1046,39 @@ private function addUniquenessFunctionCall($input) { $output = ''; - preg_match('/' . EntityDataObject::CEST_UNIQUE_FUNCTION .'\("[\w]+"\)/', $input, $matches); + preg_match('/' . EntityDataObject::CEST_UNIQUE_FUNCTION . '\("[\w]+"\)/', $input, $matches); if (!empty($matches)) { $parts = preg_split('/' . EntityDataObject::CEST_UNIQUE_FUNCTION . '\("[\w]+"\)/', $input, -1); for ($i = 0; $i < count($parts); $i++) { $parts[$i] = $this->stripWrappedQuotes($parts[$i]); } if (!empty($parts[0])) { - $output = $this->wrapWithSingleQuotes($parts[0]); + $output = $this->wrapWithDoubleQuotes($parts[0]); } $output .= $output === '' ? $matches[0] : '.' . $matches[0]; if (!empty($parts[1])) { - $output .= '.' . $this->wrapWithSingleQuotes($parts[1]); + $output .= '.' . $this->wrapWithDoubleQuotes($parts[1]); } } else { - $output = $this->wrapWithSingleQuotes($input); + $output = $this->wrapWithDoubleQuotes($input); } return $output; } /** - * Wrap input string with single quotes. + * Wrap input string with double quotes, and replaces " with \" to prevent broken PHP when generated. * * @param string $input * @return string */ - private function wrapWithSingleQuotes($input) + private function wrapWithDoubleQuotes($input) { if (empty($input)) { return ''; } - $input = addslashes($input); + //Only replace " with \" so that it doesn't break outer string. + $input = str_replace('"', '\"', $input); return sprintf('"%s"', $input); } @@ -1031,6 +1114,7 @@ private function addDollarSign($input) } // @codingStandardsIgnoreStart + /** * Wrap parameters into a function call. * diff --git a/src/Magento/FunctionalTestingFramework/Util/TestManifest.php b/src/Magento/FunctionalTestingFramework/Util/TestManifest.php new file mode 100644 index 000000000..2545141c8 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/TestManifest.php @@ -0,0 +1,59 @@ +relativeDirPath = substr($path, strlen(dirname(dirname(TESTS_BP))) + 1); + $filePath = $path . DIRECTORY_SEPARATOR . 'testManifest.txt'; + $this->filePath = $filePath; + $fileResource = fopen($filePath, 'w'); + fclose($fileResource); + } + + /** + * Takes a cest name and set of tests, records the names in a file for codeception to consume. + * + * @param string $cestName + * @param TestObject $tests + * @return void + */ + public function recordCest($cestName, $tests) + { + $fileResource = fopen($this->filePath, 'a'); + + foreach ($tests as $test) { + $line = $this->relativeDirPath . DIRECTORY_SEPARATOR . $cestName . '.php:' . $test->getName(); + fwrite($fileResource, $line ."\n"); + } + + fclose($fileResource); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/msq.php b/src/Magento/FunctionalTestingFramework/Util/msq.php index 4ab212580..621de31ac 100644 --- a/src/Magento/FunctionalTestingFramework/Util/msq.php +++ b/src/Magento/FunctionalTestingFramework/Util/msq.php @@ -14,7 +14,7 @@ function msq($id = null) return MagentoSequence::$hash[$id]; } $prefix = MagentoSequence::$prefix; - $sequence = $prefix . uniqid($id); + $sequence = $prefix . uniqid(); if ($id) { MagentoSequence::$hash[$id] = $sequence; } @@ -35,7 +35,7 @@ function msqs($id = null) return MagentoSequence::$suiteHash[$id]; } $prefix = MagentoSequence::$prefix; - $sequence = $prefix . uniqid($id); + $sequence = $prefix . uniqid(); if ($id) { MagentoSequence::$suiteHash[$id] = $sequence; }