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;
}