id="bundle-option-= $block->escapeHtmlAttr($_option->getId()) ?>-qty-input"
- class="input-text admin__control-text qty"
+ class="input-text admin__control-text qty validate-greater-than-zero"
type="text"
name="bundle_option_qty[= $block->escapeHtmlAttr($_option->getId()) ?>]"
value="= $block->escapeHtmlAttr($_defaultQty) ?>" />
diff --git a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js
index 49ee253ad1e88..207a97c270eeb 100644
--- a/app/code/Magento/Bundle/view/base/web/js/price-bundle.js
+++ b/app/code/Magento/Bundle/view/base/web/js/price-bundle.js
@@ -28,7 +28,8 @@ define([
controlContainer: 'dd', // should be eliminated
priceFormat: {},
isFixedPrice: false,
- optionTierPricesBlocksSelector: '#option-tier-prices-{1} [data-role="selection-tier-prices"]'
+ optionTierPricesBlocksSelector: '#option-tier-prices-{1} [data-role="selection-tier-prices"]',
+ isOptionsInitialized: false
};
$.widget('mage.priceBundle', {
@@ -53,20 +54,37 @@ define([
priceBox = $(this.options.priceBoxSelector, form),
qty = $(this.options.qtyFieldSelector, form);
- if (priceBox.data('magePriceBox') &&
- priceBox.priceBox('option') &&
- priceBox.priceBox('option').priceConfig
- ) {
- if (priceBox.priceBox('option').priceConfig.optionTemplate) {
- this._setOption('optionTemplate', priceBox.priceBox('option').priceConfig.optionTemplate);
+ this._updatePriceBox();
+ priceBox.on('price-box-initialized', this._updatePriceBox.bind(this));
+ options.on('change', this._onBundleOptionChanged.bind(this));
+ qty.on('change', this._onQtyFieldChanged.bind(this));
+ },
+
+ /**
+ * Update price box config with bundle option prices
+ * @private
+ */
+ _updatePriceBox: function () {
+ var form = this.element,
+ options = $(this.options.productBundleSelector, form),
+ priceBox = $(this.options.priceBoxSelector, form);
+
+ if (!this.options.isOptionsInitialized) {
+ if (priceBox.data('magePriceBox') &&
+ priceBox.priceBox('option') &&
+ priceBox.priceBox('option').priceConfig
+ ) {
+ if (priceBox.priceBox('option').priceConfig.optionTemplate) { //eslint-disable-line max-depth
+ this._setOption('optionTemplate', priceBox.priceBox('option').priceConfig.optionTemplate);
+ }
+ this._setOption('priceFormat', priceBox.priceBox('option').priceConfig.priceFormat);
+ priceBox.priceBox('setDefault', this.options.optionConfig.prices);
+ this.options.isOptionsInitialized = true;
}
- this._setOption('priceFormat', priceBox.priceBox('option').priceConfig.priceFormat);
- priceBox.priceBox('setDefault', this.options.optionConfig.prices);
+ this._applyOptionNodeFix(options);
}
- this._applyOptionNodeFix(options);
- options.on('change', this._onBundleOptionChanged.bind(this));
- qty.on('change', this._onQtyFieldChanged.bind(this));
+ return this;
},
/**
diff --git a/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php b/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php
new file mode 100644
index 0000000000000..5cdfdc88e7dc1
--- /dev/null
+++ b/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php
@@ -0,0 +1,145 @@
+pricingHelper = $pricingHelper;
+ $this->serializer = $serializer;
+ $this->configuration = $configuration;
+ }
+
+ /**
+ * Extract data for a bundled cart item
+ *
+ * @param Item $item
+ * @return array
+ */
+ public function getData(Item $item): array
+ {
+ $options = [];
+ $product = $item->getProduct();
+
+ /** @var \Magento\Bundle\Model\Product\Type $typeInstance */
+ $typeInstance = $product->getTypeInstance();
+
+ $optionsQuoteItemOption = $item->getOptionByCode('bundle_option_ids');
+ $bundleOptionsIds = $optionsQuoteItemOption
+ ? $this->serializer->unserialize($optionsQuoteItemOption->getValue())
+ : [];
+
+ if ($bundleOptionsIds) {
+ /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */
+ $optionsCollection = $typeInstance->getOptionsByIds($bundleOptionsIds, $product);
+
+ $selectionsQuoteItemOption = $item->getOptionByCode('bundle_selection_ids');
+
+ $bundleSelectionIds = $this->serializer->unserialize($selectionsQuoteItemOption->getValue());
+
+ if (!empty($bundleSelectionIds)) {
+ $selectionsCollection = $typeInstance->getSelectionsByIds($bundleSelectionIds, $product);
+ $bundleOptions = $optionsCollection->appendSelections($selectionsCollection, true);
+
+ $options = $this->buildBundleOptions($bundleOptions, $item);
+ }
+ }
+
+ return $options;
+ }
+
+ /**
+ * Build bundle product options based on current selection
+ *
+ * @param \Magento\Bundle\Model\Option[] $bundleOptions
+ * @param Item $item
+ * @return array
+ */
+ private function buildBundleOptions(array $bundleOptions, Item $item): array
+ {
+ $options = [];
+ foreach ($bundleOptions as $bundleOption) {
+ if (!$bundleOption->getSelections()) {
+ continue;
+ }
+
+ $options[] = [
+ 'id' => $bundleOption->getId(),
+ 'label' => $bundleOption->getTitle(),
+ 'type' => $bundleOption->getType(),
+ 'values' => $this->buildBundleOptionValues($bundleOption->getSelections(), $item),
+ ];
+ }
+
+ return $options;
+ }
+
+ /**
+ * Build bundle product option values based on current selection
+ *
+ * @param Product[] $selections
+ * @param Item $item
+ * @return array
+ */
+ private function buildBundleOptionValues(array $selections, Item $item): array
+ {
+ $values = [];
+
+ $product = $item->getProduct();
+ foreach ($selections as $selection) {
+ $qty = (float) $this->configuration->getSelectionQty($product, $selection->getSelectionId());
+ if (!$qty) {
+ continue;
+ }
+
+ $selectionPrice = $this->configuration->getSelectionFinalPrice($item, $selection);
+
+ $values[] = [
+ 'id' => $selection->getSelectionId(),
+ 'label' => $selection->getName(),
+ 'quantity' => $qty,
+ 'price' => $this->pricingHelper->currency($selectionPrice, false, false),
+ ];
+ }
+
+ return $values;
+ }
+}
diff --git a/app/code/Magento/BundleGraphQl/Model/Cart/BuyRequest/BundleDataProvider.php b/app/code/Magento/BundleGraphQl/Model/Cart/BuyRequest/BundleDataProvider.php
new file mode 100644
index 0000000000000..37a9309092166
--- /dev/null
+++ b/app/code/Magento/BundleGraphQl/Model/Cart/BuyRequest/BundleDataProvider.php
@@ -0,0 +1,46 @@
+arrayManager = $arrayManager;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(array $cartItemData): array
+ {
+ $bundleOptions = [];
+ $bundleInputs = $this->arrayManager->get('bundle_options', $cartItemData) ?? [];
+ foreach ($bundleInputs as $bundleInput) {
+ $bundleOptions['bundle_option'][$bundleInput['id']] = $bundleInput['value'];
+ $bundleOptions['bundle_option_qty'][$bundleInput['id']] = $bundleInput['quantity'];
+ }
+
+ return $bundleOptions;
+ }
+}
diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleOption.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleOption.php
new file mode 100644
index 0000000000000..6b64310fcb1e3
--- /dev/null
+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleOption.php
@@ -0,0 +1,46 @@
+dataProvider = $bundleOptionDataProvider;
+ }
+
+ /**
+ * @inheritdoc
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
+ {
+ if (!isset($value['model'])) {
+ throw new GraphQlInputException(__('Value must contain "model" property.'));
+ }
+ return $this->dataProvider->getData($value['model']);
+ }
+}
diff --git a/app/code/Magento/BundleGraphQl/composer.json b/app/code/Magento/BundleGraphQl/composer.json
index db85c2149ec18..74149f500df8e 100644
--- a/app/code/Magento/BundleGraphQl/composer.json
+++ b/app/code/Magento/BundleGraphQl/composer.json
@@ -7,6 +7,8 @@
"magento/module-catalog": "*",
"magento/module-bundle": "*",
"magento/module-catalog-graph-ql": "*",
+ "magento/module-quote": "*",
+ "magento/module-quote-graph-ql": "*",
"magento/module-store": "*",
"magento/framework": "*"
},
diff --git a/app/code/Magento/BundleGraphQl/etc/di.xml b/app/code/Magento/BundleGraphQl/etc/di.xml
index 4f41f3cb8dc80..15acad7c6bf06 100644
--- a/app/code/Magento/BundleGraphQl/etc/di.xml
+++ b/app/code/Magento/BundleGraphQl/etc/di.xml
@@ -16,4 +16,11 @@
+
+
+
+ - BundleCartItem
+
+
+
diff --git a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml
index 98dbe012c9002..b847a6672e046 100644
--- a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml
@@ -13,6 +13,13 @@
+
+
+
+ - Magento\BundleGraphQl\Model\Cart\BuyRequest\BundleDataProvider
+
+
+
diff --git a/app/code/Magento/BundleGraphQl/etc/module.xml b/app/code/Magento/BundleGraphQl/etc/module.xml
index 352a46d7c171e..8d6725054867e 100644
--- a/app/code/Magento/BundleGraphQl/etc/module.xml
+++ b/app/code/Magento/BundleGraphQl/etc/module.xml
@@ -8,6 +8,7 @@
+
diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls
index 74e21d3feaba2..0eff0e086180e 100644
--- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls
@@ -1,6 +1,50 @@
# Copyright © Magento, Inc. All rights reserved.
# See COPYING.txt for license details.
+type Mutation {
+ addBundleProductsToCart(input: AddBundleProductsToCartInput): AddBundleProductsToCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddSimpleProductsToCart")
+}
+
+input AddBundleProductsToCartInput {
+ cart_id: String!
+ cart_items: [BundleProductCartItemInput!]!
+}
+
+input BundleProductCartItemInput {
+ data: CartItemInput!
+ bundle_options:[BundleOptionInput!]!
+ customizable_options:[CustomizableOptionInput!]
+}
+
+input BundleOptionInput {
+ id: Int!
+ quantity: Float!
+ value: [String!]!
+}
+
+type AddBundleProductsToCartOutput {
+ cart: Cart!
+}
+
+type BundleCartItem implements CartItemInterface {
+ customizable_options: [SelectedCustomizableOption]! @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions")
+ bundle_options: [SelectedBundleOption!]! @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundleOption")
+}
+
+type SelectedBundleOption {
+ id: Int!
+ label: String!
+ type: String!
+ values: [SelectedBundleOptionValue!]!
+}
+
+type SelectedBundleOptionValue {
+ id: Int!
+ label: String!
+ quantity: Float!
+ price: Float!
+}
+
type BundleItem @doc(description: "BundleItem defines an individual item in a bundle product.") {
option_id: Int @doc(description: "An ID assigned to each type of item in a bundle product.")
title: String @doc(description: "The display name of the item.")
diff --git a/app/code/Magento/BundleImportExport/registration.php b/app/code/Magento/BundleImportExport/registration.php
index 2f68e2e05c036..db96b4d9dd470 100644
--- a/app/code/Magento/BundleImportExport/registration.php
+++ b/app/code/Magento/BundleImportExport/registration.php
@@ -4,6 +4,6 @@
* See COPYING.txt for license details.
*/
-use \Magento\Framework\Component\ComponentRegistrar;
+use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_BundleImportExport', __DIR__);
diff --git a/app/code/Magento/CacheInvalidate/registration.php b/app/code/Magento/CacheInvalidate/registration.php
index 5910edade1092..1c1ac92e330b0 100644
--- a/app/code/Magento/CacheInvalidate/registration.php
+++ b/app/code/Magento/CacheInvalidate/registration.php
@@ -4,6 +4,6 @@
* See COPYING.txt for license details.
*/
-use \Magento\Framework\Component\ComponentRegistrar;
+use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CacheInvalidate', __DIR__);
diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
index 4c974e6fced05..9103c4191544c 100644
--- a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
+++ b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml
@@ -7,7 +7,7 @@
-->
+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/Captcha/Test/Unit/CustomerData/CaptchaTest.php b/app/code/Magento/Captcha/Test/Unit/CustomerData/CaptchaTest.php
new file mode 100644
index 0000000000000..a791039fe27f9
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Unit/CustomerData/CaptchaTest.php
@@ -0,0 +1,98 @@
+helperMock = $this->createMock(CaptchaHelper::class);
+ $this->customerSessionMock = $this->createMock(CustomerSession::class);
+ $this->formIds = [
+ 'user_login'
+ ];
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->model = $this->objectManagerHelper->getObject(
+ Captcha::class,
+ [
+ 'helper' => $this->helperMock,
+ 'formIds' => $this->formIds,
+ 'customerSession' => $this->customerSessionMock
+ ]
+ );
+ }
+
+ /**
+ * Test getSectionData() when user is login and require captcha
+ */
+ public function testGetSectionDataWhenLoginAndRequireCaptcha()
+ {
+ $emailLogin = 'test@localhost.com';
+
+ $userLoginModel = $this->createMock(DefaultModel::class);
+ $userLoginModel->expects($this->any())->method('isRequired')->with($emailLogin)
+ ->willReturn(true);
+ $this->helperMock->expects($this->any())->method('getCaptcha')->with('user_login')->willReturn($userLoginModel);
+
+ $this->customerSessionMock->expects($this->any())->method('isLoggedIn')
+ ->willReturn(true);
+
+ $customerDataMock = $this->createMock(CustomerData::class);
+ $customerDataMock->expects($this->any())->method('getEmail')->willReturn($emailLogin);
+ $this->customerSessionMock->expects($this->any())->method('getCustomerData')
+ ->willReturn($customerDataMock);
+
+ /* Assert to test */
+ $this->assertEquals(
+ [
+ "user_login" => [
+ "isRequired" => true,
+ "timestamp" => time()
+ ]
+ ],
+ $this->model->getSectionData()
+ );
+ }
+}
diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Config/FontTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Config/FontTest.php
new file mode 100644
index 0000000000000..42ab3146f1321
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Unit/Model/Config/FontTest.php
@@ -0,0 +1,101 @@
+helperDataMock = $this->createMock(HelperData::class);
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->model = $this->objectManagerHelper->getObject(
+ Font::class,
+ [
+ 'captchaData' => $this->helperDataMock
+ ]
+ );
+ }
+
+ /**
+ * Test toOptionArray() with data provider below
+ *
+ * @param array $fonts
+ * @param array $expectedResult
+ * @dataProvider toOptionArrayDataProvider
+ */
+ public function testToOptionArray($fonts, $expectedResult)
+ {
+ $this->helperDataMock->expects($this->any())->method('getFonts')
+ ->willReturn($fonts);
+
+ $this->assertEquals($expectedResult, $this->model->toOptionArray());
+ }
+
+ /**
+ * Data Provider for testing toOptionArray()
+ *
+ * @return array
+ */
+ public function toOptionArrayDataProvider()
+ {
+ return [
+ 'Empty get font' => [
+ [],
+ []
+ ],
+ 'Get font result' => [
+ [
+ 'arial' => [
+ 'label' => 'Arial',
+ 'path' => '/www/magento/fonts/arial.ttf'
+ ],
+ 'verdana' => [
+ 'label' => 'Verdana',
+ 'path' => '/www/magento/fonts/verdana.ttf'
+ ]
+ ],
+ [
+ [
+ 'label' => 'Arial',
+ 'value' => 'arial'
+ ],
+ [
+ 'label' => 'Verdana',
+ 'value' => 'verdana'
+ ]
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/BackendTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/BackendTest.php
new file mode 100644
index 0000000000000..054cc71af61bc
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/BackendTest.php
@@ -0,0 +1,100 @@
+configMock = $this->createMock(ScopeConfigInterface::class);
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->model = $this->objectManagerHelper->getObject(
+ Backend::class,
+ [
+ 'config' => $this->configMock
+ ]
+ );
+ }
+
+ /**
+ * Test toOptionArray() with data provider below
+ *
+ * @param string|array $config
+ * @param array $expectedResult
+ * @dataProvider toOptionArrayDataProvider
+ */
+ public function testToOptionArray($config, $expectedResult)
+ {
+ $this->configMock->expects($this->any())->method('getValue')
+ ->with('captcha/backend/areas', 'default')
+ ->willReturn($config);
+
+ $this->assertEquals($expectedResult, $this->model->toOptionArray());
+ }
+
+ /**
+ * Data Provider for testing toOptionArray()
+ *
+ * @return array
+ */
+ public function toOptionArrayDataProvider()
+ {
+ return [
+ 'Empty captcha backend areas' => [
+ '',
+ []
+ ],
+ 'With two captcha backend area' => [
+ [
+ 'backend_login' => [
+ 'label' => 'Admin Login'
+ ],
+ 'backend_forgotpassword' => [
+ 'label' => 'Admin Forgot Password'
+ ]
+ ],
+ [
+ [
+ 'label' => 'Admin Login',
+ 'value' => 'backend_login'
+ ],
+ [
+ 'label' => 'Admin Forgot Password',
+ 'value' => 'backend_forgotpassword'
+ ]
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/FrontendTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/FrontendTest.php
new file mode 100644
index 0000000000000..d3f40f5872a7d
--- /dev/null
+++ b/app/code/Magento/Captcha/Test/Unit/Model/Config/Form/FrontendTest.php
@@ -0,0 +1,100 @@
+configMock = $this->createMock(ScopeConfigInterface::class);
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->model = $this->objectManagerHelper->getObject(
+ Frontend::class,
+ [
+ 'config' => $this->configMock
+ ]
+ );
+ }
+
+ /**
+ * Test toOptionArray() with data provider below
+ *
+ * @param string|array $config
+ * @param array $expectedResult
+ * @dataProvider toOptionArrayDataProvider
+ */
+ public function testToOptionArray($config, $expectedResult)
+ {
+ $this->configMock->expects($this->any())->method('getValue')
+ ->with('captcha/frontend/areas', 'default')
+ ->willReturn($config);
+
+ $this->assertEquals($expectedResult, $this->model->toOptionArray());
+ }
+
+ /**
+ * Data Provider for testing toOptionArray()
+ *
+ * @return array
+ */
+ public function toOptionArrayDataProvider()
+ {
+ return [
+ 'Empty captcha frontend areas' => [
+ '',
+ []
+ ],
+ 'With two captcha frontend area' => [
+ [
+ 'product_sendtofriend_form' => [
+ 'label' => 'Send To Friend Form'
+ ],
+ 'sales_rule_coupon_request' => [
+ 'label' => 'Applying coupon code'
+ ]
+ ],
+ [
+ [
+ 'label' => 'Send To Friend Form',
+ 'value' => 'product_sendtofriend_form'
+ ],
+ [
+ 'label' => 'Applying coupon code',
+ 'value' => 'sales_rule_coupon_request'
+ ]
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Captcha/registration.php b/app/code/Magento/Captcha/registration.php
index d6c49c719c969..6721e4abcec61 100644
--- a/app/code/Magento/Captcha/registration.php
+++ b/app/code/Magento/Captcha/registration.php
@@ -4,6 +4,6 @@
* See COPYING.txt for license details.
*/
-use \Magento\Framework\Component\ComponentRegistrar;
+use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_Captcha', __DIR__);
diff --git a/app/code/Magento/CardinalCommerce/Test/Mftf/Page/AdminThreeDSecurePage.xml b/app/code/Magento/CardinalCommerce/Test/Mftf/Page/AdminThreeDSecurePage.xml
new file mode 100644
index 0000000000000..dae6869dbfe79
--- /dev/null
+++ b/app/code/Magento/CardinalCommerce/Test/Mftf/Page/AdminThreeDSecurePage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CardinalCommerce/Test/Mftf/Section/AdminCardinalCommerceSection.xml b/app/code/Magento/CardinalCommerce/Test/Mftf/Section/AdminCardinalCommerceSection.xml
new file mode 100644
index 0000000000000..1016fbaefb0ab
--- /dev/null
+++ b/app/code/Magento/CardinalCommerce/Test/Mftf/Section/AdminCardinalCommerceSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/CardinalCommerce/Test/Mftf/Test/AdminCardinalCommerceSettingsHiddenTest.xml b/app/code/Magento/CardinalCommerce/Test/Mftf/Test/AdminCardinalCommerceSettingsHiddenTest.xml
new file mode 100644
index 0000000000000..a41b96f0db6e4
--- /dev/null
+++ b/app/code/Magento/CardinalCommerce/Test/Mftf/Test/AdminCardinalCommerceSettingsHiddenTest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtParserTest.php b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtParserTest.php
new file mode 100644
index 0000000000000..7c17c4e2e87d5
--- /dev/null
+++ b/app/code/Magento/CardinalCommerce/Test/Unit/Model/Response/JwtParserTest.php
@@ -0,0 +1,131 @@
+objectManager = new ObjectManager($this);
+
+ $this->configMock = $this->getMockBuilder(Config::class)
+ ->setMethods(['getApiKey', 'isDebugModeEnabled'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->jwtManagementMock = $this->getMockBuilder(JwtManagement::class)
+ ->setMethods(['decode'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->jwtPayloadValidatorMock = $this->getMockBuilder(JwtPayloadValidatorInterface::class)
+ ->setMethods(['validate'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->model = $this->objectManager->getObject(
+ JwtParser::class,
+ [
+ 'jwtManagement' => $this->jwtManagementMock,
+ 'config' => $this->configMock,
+ 'tokenValidator' => $this->jwtPayloadValidatorMock
+ ]
+ );
+
+ $this->configMock->expects($this->any())
+ ->method('getApiKey')
+ ->willReturn('API Key');
+
+ $this->configMock->expects($this->any())
+ ->method('isDebugModeEnabled')
+ ->willReturn(false);
+
+ $this->jwtManagementMock->expects($this->any())
+ ->method('decode')
+ ->with('string_to_test', 'API Key')
+ ->willReturn(['mockResult' => 'jwtPayload']);
+ }
+
+ /**
+ * Tests Jwt Parser execute with the result and no exception.
+ */
+ public function testExecuteWithNoException()
+ {
+ /* Validate Success */
+ $this->jwtPayloadValidatorMock->expects($this->any())
+ ->method('validate')
+ ->with(['mockResult' => 'jwtPayload'])
+ ->willReturn(true);
+
+ /* Assert the result of function */
+ $jwtPayload = $this->model->execute('string_to_test');
+ $this->assertEquals(
+ ['mockResult' => 'jwtPayload'],
+ $jwtPayload
+ );
+ }
+
+ /**
+ * Tests Jwt Parser execute with exception and no result.
+ */
+ public function testExecuteWithException()
+ {
+ /* Validate Fail */
+ $this->jwtPayloadValidatorMock->expects($this->any())
+ ->method('validate')
+ ->with(['mockResult' => 'jwtPayload'])
+ ->willReturn(false);
+
+ $this->expectException(LocalizedException::class);
+ $this->expectExceptionMessage(
+ 'Authentication Failed. Your card issuer cannot authenticate this card. ' .
+ 'Please select another card or form of payment to complete your purchase.'
+ );
+
+ /* Execute function */
+ $this->model->execute('string_to_test');
+ }
+}
diff --git a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml
index 532fcdd0f598f..046475baba676 100644
--- a/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml
+++ b/app/code/Magento/CardinalCommerce/etc/adminhtml/system.xml
@@ -19,26 +19,41 @@
Magento\CardinalCommerce\Model\Adminhtml\Source\Environment
three_d_secure/cardinal/environment
+
+ 1
+
three_d_secure/cardinal/org_unit_id
Magento\Config\Model\Config\Backend\Encrypted
+
+ 1
+
three_d_secure/cardinal/api_key
Magento\Config\Model\Config\Backend\Encrypted
+
+ 1
+
three_d_secure/cardinal/api_identifier
Magento\Config\Model\Config\Backend\Encrypted
+
+ 1
+
Magento\Config\Model\Config\Source\Yesno
three_d_secure/cardinal/debug
+
+ 1
+
diff --git a/app/code/Magento/CardinalCommerce/registration.php b/app/code/Magento/CardinalCommerce/registration.php
index 26fb168fb0ae2..714c7692cf4b5 100644
--- a/app/code/Magento/CardinalCommerce/registration.php
+++ b/app/code/Magento/CardinalCommerce/registration.php
@@ -4,6 +4,6 @@
* See COPYING.txt for license details.
*/
-use \Magento\Framework\Component\ComponentRegistrar;
+use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_CardinalCommerce', __DIR__);
diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php
index 43e0de4f20176..d111de1b04b94 100644
--- a/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php
+++ b/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php
@@ -29,6 +29,7 @@ public function getFinalPrice();
/**
* Set the final price: usually it calculated as minimal price of the product
+ *
* Can be different depends on type of product
*
* @param string $finalPrice
@@ -39,6 +40,7 @@ public function setFinalPrice($finalPrice);
/**
* Retrieve max price of a product
+ *
* E.g. for product with custom options is price with the most expensive custom option
*
* @return string
@@ -57,6 +59,7 @@ public function setMaxPrice($maxPrice);
/**
* Retrieve the minimal price of the product or variation
+ *
* The minimal price is for example, the lowest price of all variations for complex product
*
* @return string
@@ -66,7 +69,7 @@ public function getMinimalPrice();
/**
* Set max regular price
- * Max regular price is the same, as maximum price, except of excluding calculating special price and catalogules
+ * Max regular price is the same, as maximum price, except of excluding calculating special price and catalog rules
* in it
*
* @param string $maxRegularPrice
@@ -130,6 +133,7 @@ public function setMinimalPrice($minimalPrice);
/**
* Regular price - is price of product without discounts and special price with taxes and fixed product tax
+ *
* Usually this price is corresponding to price in admin panel of product
*
* @return string
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php
index 9a4a9fa768006..929c181bf820c 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php
@@ -407,6 +407,7 @@ protected function _getNodeJson($node, $level = 0)
public function buildNodeName($node)
{
$result = $this->escapeHtml($node->getName());
+ $result .= ' (ID: ' . $node->getId() . ')';
if ($this->_withProductCount) {
$result .= ' (' . $node->getProductCount() . ')';
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php
index 1b6756968662f..89239a2e3e608 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Advanced.php
@@ -7,16 +7,20 @@
namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab;
use Magento\Backend\Block\Widget\Form\Generic;
+use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
use Magento\Config\Model\Config\Source\Yesno;
use Magento\Eav\Block\Adminhtml\Attribute\PropertyLocker;
use Magento\Eav\Helper\Data;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Stdlib\DateTime;
/**
- * Product attribute add/edit form main tab
+ * Product attribute add/edit advanced form tab
*
* @api
* @since 100.0.2
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Advanced extends Generic
{
@@ -70,7 +74,7 @@ public function __construct(
* Adding product form elements for editing attribute
*
* @return $this
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
* @SuppressWarnings(PHPMD)
*/
protected function _prepareForm()
@@ -139,7 +143,21 @@ protected function _prepareForm()
'label' => __('Default Value'),
'title' => __('Default Value'),
'value' => $attributeObject->getDefaultValue(),
- 'date_format' => $dateFormat
+ 'date_format' => $dateFormat,
+ ]
+ );
+
+ $timeFormat = $this->_localeDate->getTimeFormat(\IntlDateFormatter::SHORT);
+ $fieldset->addField(
+ 'default_value_datetime',
+ 'date',
+ [
+ 'name' => 'default_value_datetime',
+ 'label' => __('Default Value'),
+ 'title' => __('Default Value'),
+ 'value' => $this->getLocalizedDateDefaultValue(),
+ 'date_format' => $dateFormat,
+ 'time_format' => $timeFormat,
]
);
@@ -266,7 +284,7 @@ protected function _initFormValues()
/**
* Retrieve attribute object from registry
*
- * @return mixed
+ * @return Attribute
*/
private function getAttributeObject()
{
@@ -285,4 +303,28 @@ private function getPropertyLocker()
}
return $this->propertyLocker;
}
+
+ /**
+ * Get localized date default value
+ *
+ * @return string
+ * @throws LocalizedException
+ */
+ private function getLocalizedDateDefaultValue(): string
+ {
+ $attributeObject = $this->getAttributeObject();
+ if (empty($attributeObject->getDefaultValue()) || $attributeObject->getFrontendInput() !== 'datetime') {
+ return (string)$attributeObject->getDefaultValue();
+ }
+
+ try {
+ $localizedDate = $this->_localeDate->date($attributeObject->getDefaultValue(), null, false);
+ $localizedDate->setTimezone(new \DateTimeZone($this->_localeDate->getConfigTimezone()));
+ $localizedDate = $localizedDate->format(DateTime::DATETIME_PHP_FORMAT);
+ } catch (\Exception $e) {
+ throw new LocalizedException(__('The default date is invalid.'));
+ }
+
+ return $localizedDate;
+ }
}
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php
index 85cf37a1214b5..955ea259ec9a8 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit/Tab/Main.php
@@ -7,60 +7,60 @@
/**
* Product attribute add/edit form main tab
*
- * @author Magento Core Team
+ * @author Magento Core Team
*/
namespace Magento\Catalog\Block\Adminhtml\Product\Attribute\Edit\Tab;
+use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Apply as HelperApply;
use Magento\Eav\Block\Adminhtml\Attribute\Edit\Main\AbstractMain;
+use Magento\Framework\Data\Form\Element\AbstractElement;
+use Magento\Framework\Data\Form\Element\Fieldset;
+use Magento\Framework\DataObject;
/**
+ * Product attribute add/edit form main tab
+ *
* @api
- * @SuppressWarnings(PHPMD.DepthOfInheritance)
* @since 100.0.2
*/
class Main extends AbstractMain
{
/**
- * Adding product form elements for editing attribute
- *
- * @return $this
- * @SuppressWarnings(PHPMD.UnusedLocalVariable)
+ * @inheritdoc
*/
protected function _prepareForm()
{
parent::_prepareForm();
- /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attributeObject */
- $attributeObject = $this->getAttributeObject();
- /* @var $form \Magento\Framework\Data\Form */
- $form = $this->getForm();
- /* @var $fieldset \Magento\Framework\Data\Form\Element\Fieldset */
- $fieldset = $form->getElement('base_fieldset');
- $fieldsToRemove = ['attribute_code', 'is_unique', 'frontend_class'];
- foreach ($fieldset->getElements() as $element) {
- /** @var \Magento\Framework\Data\Form\AbstractForm $element */
- if (substr($element->getId(), 0, strlen('default_value')) == 'default_value') {
- $fieldsToRemove[] = $element->getId();
- }
- }
- foreach ($fieldsToRemove as $id) {
- $fieldset->removeField($id);
- }
+ $this->removeUnusedFields();
+ $this->processFrontendInputTypes();
+
+ $this->_eventManager->dispatch('product_attribute_form_build_main_tab', ['form' => $this->getForm()]);
+
+ return $this;
+ }
+ /**
+ * @inheritdoc
+ */
+ protected function _getAdditionalElementTypes()
+ {
+ return ['apply' => HelperApply::class];
+ }
+
+ /**
+ * Process frontend input types for product attributes
+ *
+ * @return void
+ */
+ private function processFrontendInputTypes(): void
+ {
+ $form = $this->getForm();
+ /** @var AbstractElement $frontendInputElm */
$frontendInputElm = $form->getElement('frontend_input');
- $additionalTypes = [
- ['value' => 'price', 'label' => __('Price')],
- ['value' => 'media_image', 'label' => __('Media Image')],
- ];
- $additionalReadOnlyTypes = ['gallery' => __('Gallery')];
- if (isset($additionalReadOnlyTypes[$attributeObject->getFrontendInput()])) {
- $additionalTypes[] = [
- 'value' => $attributeObject->getFrontendInput(),
- 'label' => $additionalReadOnlyTypes[$attributeObject->getFrontendInput()],
- ];
- }
+ $additionalTypes = $this->getAdditionalFrontendInputTypes();
- $response = new \Magento\Framework\DataObject();
+ $response = new DataObject();
$response->setTypes([]);
$this->_eventManager->dispatch('adminhtml_product_attribute_types', ['response' => $response]);
$_hiddenFields = [];
@@ -74,19 +74,51 @@ protected function _prepareForm()
$frontendInputValues = array_merge($frontendInputElm->getValues(), $additionalTypes);
$frontendInputElm->setValues($frontendInputValues);
+ }
- $this->_eventManager->dispatch('product_attribute_form_build_main_tab', ['form' => $form]);
+ /**
+ * Get additional Frontend Input Types for product attributes
+ *
+ * @return array
+ */
+ private function getAdditionalFrontendInputTypes(): array
+ {
+ $additionalTypes = [
+ ['value' => 'price', 'label' => __('Price')],
+ ['value' => 'media_image', 'label' => __('Media Image')],
+ ];
- return $this;
+ $additionalReadOnlyTypes = ['gallery' => __('Gallery')];
+ $attributeObject = $this->getAttributeObject();
+ if (isset($additionalReadOnlyTypes[$attributeObject->getFrontendInput()])) {
+ $additionalTypes[] = [
+ 'value' => $attributeObject->getFrontendInput(),
+ 'label' => $additionalReadOnlyTypes[$attributeObject->getFrontendInput()],
+ ];
+ }
+
+ return $additionalTypes;
}
/**
- * Retrieve additional element types for product attributes
+ * Remove unused form fields
*
- * @return array
+ * @return void
*/
- protected function _getAdditionalElementTypes()
+ private function removeUnusedFields(): void
{
- return ['apply' => \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Apply::class];
+ $form = $this->getForm();
+ /* @var $fieldset Fieldset */
+ $fieldset = $form->getElement('base_fieldset');
+ $fieldsToRemove = ['attribute_code', 'is_unique', 'frontend_class'];
+ foreach ($fieldset->getElements() as $element) {
+ /** @var AbstractElement $element */
+ if (substr($element->getId(), 0, strlen('default_value')) === 'default_value') {
+ $fieldsToRemove[] = $element->getId();
+ }
+ }
+ foreach ($fieldsToRemove as $id) {
+ $fieldset->removeField($id);
+ }
}
}
diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php
index 030b6e1d2204c..0bfdcc678e9f7 100644
--- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php
+++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php
@@ -3,16 +3,14 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-
-/**
- * Product options abstract type block
- *
- * @author Magento Core Team
- */
+declare(strict_types=1);
namespace Magento\Catalog\Block\Product\View\Options;
+use Magento\Catalog\Pricing\Price\BasePrice;
+use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule;
use Magento\Catalog\Pricing\Price\CustomOptionPriceInterface;
+use Magento\Framework\App\ObjectManager;
/**
* Product options section abstract block.
@@ -47,20 +45,29 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template
*/
protected $_catalogHelper;
+ /**
+ * @var CalculateCustomOptionCatalogRule
+ */
+ private $calculateCustomOptionCatalogRule;
+
/**
* @param \Magento\Framework\View\Element\Template\Context $context
* @param \Magento\Framework\Pricing\Helper\Data $pricingHelper
* @param \Magento\Catalog\Helper\Data $catalogData
* @param array $data
+ * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule
*/
public function __construct(
\Magento\Framework\View\Element\Template\Context $context,
\Magento\Framework\Pricing\Helper\Data $pricingHelper,
\Magento\Catalog\Helper\Data $catalogData,
- array $data = []
+ array $data = [],
+ CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null
) {
$this->pricingHelper = $pricingHelper;
$this->_catalogHelper = $catalogData;
+ $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule
+ ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class);
parent::__construct($context, $data);
}
@@ -161,6 +168,15 @@ protected function _formatPrice($value, $flag = true)
$priceStr = $sign;
$customOptionPrice = $this->getProduct()->getPriceInfo()->getPrice('custom_option_price');
+
+ if (!$value['is_percent']) {
+ $value['pricing_value'] = $this->calculateCustomOptionCatalogRule->execute(
+ $this->getProduct(),
+ (float)$value['pricing_value'],
+ (bool)$value['is_percent']
+ );
+ }
+
$context = [CustomOptionPriceInterface::CONFIGURATION_OPTION_FLAG => true];
$optionAmount = $customOptionPrice->getCustomAmount($value['pricing_value'], null, $context);
$priceStr .= $this->getLayout()->getBlock('product.price.render.default')->renderAmount(
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php
index f45f854be0761..de8c402c7e761 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Category.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category.php
@@ -82,7 +82,7 @@ public function __construct(
}
/**
- * Initialize requested category and put it into registry
+ * Initialize requested category and put it into registry.
*
* Root category can be returned, if inappropriate store/category is specified
*
@@ -111,6 +111,8 @@ protected function _initCategory($getRootInstead = false)
}
}
+ $this->registry->unregister('category');
+ $this->registry->unregister('current_category');
$this->registry->register('category', $category);
$this->registry->register('current_category', $category);
$this->wysiwigConfig->setStoreId($storeId);
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php
index ee8abfdb7ab48..9684938d38477 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Save.php
@@ -183,29 +183,29 @@ public function execute()
$products = json_decode($categoryPostData['category_products'], true);
$category->setPostedProducts($products);
}
- $this->_eventManager->dispatch(
- 'catalog_category_prepare_save',
- ['category' => $category, 'request' => $this->getRequest()]
- );
- /**
- * Check "Use Default Value" checkboxes values
- */
- if (isset($categoryPostData['use_default']) && !empty($categoryPostData['use_default'])) {
- foreach ($categoryPostData['use_default'] as $attributeCode => $attributeValue) {
- if ($attributeValue) {
- $category->setData($attributeCode, null);
+ try {
+ $this->_eventManager->dispatch(
+ 'catalog_category_prepare_save',
+ ['category' => $category, 'request' => $this->getRequest()]
+ );
+ /**
+ * Check "Use Default Value" checkboxes values
+ */
+ if (isset($categoryPostData['use_default']) && !empty($categoryPostData['use_default'])) {
+ foreach ($categoryPostData['use_default'] as $attributeCode => $attributeValue) {
+ if ($attributeValue) {
+ $category->setData($attributeCode, null);
+ }
}
}
- }
- /**
- * Proceed with $_POST['use_config']
- * set into category model for processing through validation
- */
- $category->setData('use_post_data_config', $useConfig);
+ /**
+ * Proceed with $_POST['use_config']
+ * set into category model for processing through validation
+ */
+ $category->setData('use_post_data_config', $useConfig);
- try {
$categoryResource = $category->getResource();
if ($category->hasCustomDesignTo()) {
$categoryResource->getAttribute('custom_design_from')->setMaxValue($category->getCustomDesignTo());
@@ -231,10 +231,16 @@ public function execute()
$category->save();
$this->messageManager->addSuccessMessage(__('You saved the category.'));
+ // phpcs:disable Magento2.Exceptions.ThrowCatch
} catch (\Magento\Framework\Exception\LocalizedException $e) {
$this->messageManager->addExceptionMessage($e);
$this->logger->critical($e);
$this->_getSession()->setCategoryData($categoryPostData);
+ // phpcs:disable Magento2.Exceptions.ThrowCatch
+ } catch (\Throwable $e) {
+ $this->messageManager->addErrorMessage(__('Something went wrong while saving the category.'));
+ $this->logger->critical($e);
+ $this->_getSession()->setCategoryData($categoryPostData);
}
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php
index ce6668229d658..dcb7074c0d036 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php
@@ -252,7 +252,7 @@ private function checkUniqueOption(DataObject $response, array $options = null)
private function checkEmptyOption(DataObject $response, array $optionsForCheck = null)
{
foreach ($optionsForCheck as $optionValues) {
- if (isset($optionValues[0]) && $optionValues[0] == '') {
+ if (isset($optionValues[0]) && trim($optionValues[0]) == '') {
$this->setMessageToResponse($response, [__("The value of Admin scope can't be empty.")]);
$response->setError(true);
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php
index 78ad9f423871f..1f959a22b1ba1 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Builder.php
@@ -115,6 +115,9 @@ public function build(RequestInterface $request): ProductInterface
$store = $this->storeFactory->create();
$store->load($storeId);
+ $this->registry->unregister('product');
+ $this->registry->unregister('current_product');
+ $this->registry->unregister('current_store');
$this->registry->register('product', $product);
$this->registry->register('current_product', $product);
$this->registry->register('current_store', $store);
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php
index f4c7891d00849..d43b313c43b3e 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Gallery/Upload.php
@@ -89,11 +89,6 @@ public function execute()
['fileId' => 'image']
);
$uploader->setAllowedExtensions($this->getAllowedExtensions());
-
- if (!$uploader->checkMimeType($this->getAllowedMimeTypes())) {
- throw new LocalizedException(__('Disallowed File Type.'));
- }
-
$imageAdapter = $this->adapterFactory->create();
$uploader->addValidateCallback('catalog_product_image', $imageAdapter, 'validateUploadFile');
$uploader->setAllowRenameFiles(true);
@@ -133,14 +128,4 @@ private function getAllowedExtensions()
{
return array_keys($this->allowedMimeTypes);
}
-
- /**
- * Get the set of allowed mime types.
- *
- * @return array
- */
- private function getAllowedMimeTypes()
- {
- return array_values($this->allowedMimeTypes);
- }
}
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php
index a29d02f5e545d..2ae97223d6359 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php
@@ -6,7 +6,6 @@
namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization;
-use DateTime;
use Magento\Backend\Helper\Js;
use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory as CustomOptionFactory;
use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory as ProductLinkFactory;
@@ -15,6 +14,8 @@
use Magento\Catalog\Api\ProductRepositoryInterface\Proxy as ProductRepository;
use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter;
use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Authorization as ProductAuthorization;
+use Magento\Catalog\Model\Product\Filter\DateTime as DateTimeFilter;
use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks;
use Magento\Catalog\Model\Product\Link\Resolver as LinkResolver;
use Magento\Catalog\Model\Product\LinkTypeProvider;
@@ -30,6 +31,7 @@
*
* @api
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.TooManyFields)
* @since 100.0.2
*/
class Helper
@@ -88,11 +90,6 @@ class Helper
*/
private $linkResolver;
- /**
- * @var \Magento\Framework\Stdlib\DateTime\Filter\DateTime
- */
- private $dateTimeFilter;
-
/**
* @var LinkTypeProvider
*/
@@ -103,11 +100,21 @@ class Helper
*/
private $attributeFilter;
+ /**
+ * @var ProductAuthorization
+ */
+ private $productAuthorization;
+
/**
* @var FormatInterface
*/
private $localeFormat;
+ /**
+ * @var DateTimeFilter
+ */
+ private $dateTimeFilter;
+
/**
* Constructor
*
@@ -123,6 +130,8 @@ class Helper
* @param LinkTypeProvider|null $linkTypeProvider
* @param AttributeFilter|null $attributeFilter
* @param FormatInterface|null $localeFormat
+ * @param ProductAuthorization|null $productAuthorization
+ * @param DateTimeFilter|null $dateTimeFilter
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -137,7 +146,9 @@ public function __construct(
ProductRepositoryInterface $productRepository = null,
LinkTypeProvider $linkTypeProvider = null,
AttributeFilter $attributeFilter = null,
- FormatInterface $localeFormat = null
+ FormatInterface $localeFormat = null,
+ ?ProductAuthorization $productAuthorization = null,
+ ?DateTimeFilter $dateTimeFilter = null
) {
$this->request = $request;
$this->storeManager = $storeManager;
@@ -153,6 +164,8 @@ public function __construct(
$this->linkTypeProvider = $linkTypeProvider ?: $objectManager->get(LinkTypeProvider::class);
$this->attributeFilter = $attributeFilter ?: $objectManager->get(AttributeFilter::class);
$this->localeFormat = $localeFormat ?: $objectManager->get(FormatInterface::class);
+ $this->productAuthorization = $productAuthorization ?? $objectManager->get(ProductAuthorization::class);
+ $this->dateTimeFilter = $dateTimeFilter ?? $objectManager->get(DateTimeFilter::class);
}
/**
@@ -176,7 +189,6 @@ public function initializeFromData(Product $product, array $productData)
}
$productData = $this->normalize($productData);
- $productData = $this->convertSpecialFromDateStringToObject($productData);
if (!empty($productData['is_downloadable'])) {
$productData['product_has_weight'] = 0;
@@ -200,7 +212,7 @@ public function initializeFromData(Product $product, array $productData)
foreach ($attributes as $attrKey => $attribute) {
if ($attribute->getBackend()->getType() == 'datetime') {
if (array_key_exists($attrKey, $productData) && $productData[$attrKey] != '') {
- $dateFieldFilters[$attrKey] = $this->getDateTimeFilter();
+ $dateFieldFilters[$attrKey] = $this->dateTimeFilter;
}
}
}
@@ -243,8 +255,10 @@ public function initializeFromData(Product $product, array $productData)
public function initialize(Product $product)
{
$productData = $this->request->getPost('product', []);
+ $product = $this->initializeFromData($product, $productData);
+ $this->productAuthorization->authorizeSavingOf($product);
- return $this->initializeFromData($product, $productData);
+ return $product;
}
/**
@@ -397,22 +411,6 @@ private function getLinkResolver()
return $this->linkResolver;
}
- /**
- * Get DateTimeFilter instance
- *
- * @return \Magento\Framework\Stdlib\DateTime\Filter\DateTime
- * @deprecated 101.0.0
- */
- private function getDateTimeFilter()
- {
- if ($this->dateTimeFilter === null) {
- $this->dateTimeFilter = ObjectManager::getInstance()
- ->get(\Magento\Framework\Stdlib\DateTime\Filter\DateTime::class);
- }
-
- return $this->dateTimeFilter;
- }
-
/**
* Remove ids of non selected websites from $websiteIds array and return filtered data
*
@@ -486,20 +484,4 @@ function ($valueData) {
return $product->setOptions($customOptions);
}
-
- /**
- * Convert string date presentation into object
- *
- * @param array $productData
- * @return array
- */
- private function convertSpecialFromDateStringToObject($productData)
- {
- if (isset($productData['special_from_date']) && $productData['special_from_date'] != '') {
- $productData['special_from_date'] = $this->getDateTimeFilter()->filter($productData['special_from_date']);
- $productData['special_from_date'] = new DateTime($productData['special_from_date']);
- }
-
- return $productData;
- }
}
diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php
index 770a306431b7a..eea448e0fdb0b 100644
--- a/app/code/Magento/Catalog/Controller/Category/View.php
+++ b/app/code/Magento/Catalog/Controller/Category/View.php
@@ -30,6 +30,7 @@
use Magento\Framework\View\Result\PageFactory;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;
+use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager;
/**
* View a category on storefront. Needs to be accessible by POST because of the store switching.
@@ -96,6 +97,11 @@ class View extends Action implements HttpGetActionInterface, HttpPostActionInter
*/
private $toolbarMemorizer;
+ /**
+ * @var LayoutUpdateManager
+ */
+ private $customLayoutManager;
+
/**
* @var CategoryHelper
*/
@@ -119,7 +125,8 @@ class View extends Action implements HttpGetActionInterface, HttpPostActionInter
* @param ForwardFactory $resultForwardFactory
* @param Resolver $layerResolver
* @param CategoryRepositoryInterface $categoryRepository
- * @param ToolbarMemorizer $toolbarMemorizer
+ * @param ToolbarMemorizer|null $toolbarMemorizer
+ * @param LayoutUpdateManager|null $layoutUpdateManager
* @param CategoryHelper $categoryHelper
* @param LoggerInterface $logger
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
@@ -136,6 +143,7 @@ public function __construct(
Resolver $layerResolver,
CategoryRepositoryInterface $categoryRepository,
ToolbarMemorizer $toolbarMemorizer = null,
+ ?LayoutUpdateManager $layoutUpdateManager = null,
CategoryHelper $categoryHelper = null,
LoggerInterface $logger = null
) {
@@ -149,8 +157,9 @@ public function __construct(
$this->resultForwardFactory = $resultForwardFactory;
$this->layerResolver = $layerResolver;
$this->categoryRepository = $categoryRepository;
- $this->toolbarMemorizer = $toolbarMemorizer ?: ObjectManager::getInstance()
- ->get(ToolbarMemorizer::class);
+ $this->toolbarMemorizer = $toolbarMemorizer ?: ObjectManager::getInstance()->get(ToolbarMemorizer::class);
+ $this->customLayoutManager = $layoutUpdateManager
+ ?? ObjectManager::getInstance()->get(LayoutUpdateManager::class);
$this->categoryHelper = $categoryHelper ?: ObjectManager::getInstance()
->get(CategoryHelper::class);
$this->logger = $logger ?: ObjectManager::getInstance()
@@ -279,5 +288,10 @@ private function applyLayoutUpdates(
$page->addPageLayoutHandles(['layout_update' => sha1($layoutUpdate)], null, false);
}
}
+
+ //Selected files
+ if ($settings->getPageLayoutHandles()) {
+ $page->addPageLayoutHandles($settings->getPageLayoutHandles());
+ }
}
}
diff --git a/app/code/Magento/Catalog/Helper/Product/Edit/Action/Attribute.php b/app/code/Magento/Catalog/Helper/Product/Edit/Action/Attribute.php
index 2c1bacdb99e12..09d53427a3043 100644
--- a/app/code/Magento/Catalog/Helper/Product/Edit/Action/Attribute.php
+++ b/app/code/Magento/Catalog/Helper/Product/Edit/Action/Attribute.php
@@ -3,15 +3,15 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
-/**
- * Adminhtml catalog product action attribute update helper
- */
namespace Magento\Catalog\Helper\Product\Edit\Action;
/**
- * Class Attribute
+ * Adminhtml catalog product action attribute update helper.
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class Attribute extends \Magento\Backend\Helper\Data
{
@@ -32,7 +32,7 @@ class Attribute extends \Magento\Backend\Helper\Data
/**
* Excluded from batch update attribute codes
*
- * @var string[]
+ * @var array
*/
protected $_excludedAttributes = ['url_key'];
@@ -92,6 +92,7 @@ public function __construct(
/**
* Return product collection with selected product filter
+ *
* Product collection didn't load
*
* @return \Magento\Catalog\Model\ResourceModel\Product\Collection
@@ -171,8 +172,8 @@ public function getAttributes()
$this->getProductsSetIds()
);
- if ($this->_excludedAttributes) {
- $this->_attributes->addFieldToFilter('attribute_code', ['nin' => $this->_excludedAttributes]);
+ if ($excludedAttributes = $this->getExcludedAttributes()) {
+ $this->_attributes->addFieldToFilter('attribute_code', ['nin' => $excludedAttributes]);
}
// check product type apply to limitation and remove attributes that impossible to change in mass-update
@@ -193,11 +194,24 @@ public function getAttributes()
}
/**
+ * Gets website id.
+ *
* @param int $storeId
* @return int
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getStoreWebsiteId($storeId)
{
return $this->_storeManager->getStore($storeId)->getWebsiteId();
}
+
+ /**
+ * Retrieve excluded attributes.
+ *
+ * @return array
+ */
+ public function getExcludedAttributes(): array
+ {
+ return $this->_excludedAttributes;
+ }
}
diff --git a/app/code/Magento/Catalog/Helper/Product/View.php b/app/code/Magento/Catalog/Helper/Product/View.php
index 74f40a18971d5..cf5b15cadc997 100644
--- a/app/code/Magento/Catalog/Helper/Product/View.php
+++ b/app/code/Magento/Catalog/Helper/Product/View.php
@@ -6,6 +6,8 @@
namespace Magento\Catalog\Helper\Product;
+use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\View\Result\Page as ResultPage;
/**
@@ -66,6 +68,11 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper
*/
private $string;
+ /**
+ * @var LayoutUpdateManager
+ */
+ private $layoutUpdateManager;
+
/**
* Constructor
*
@@ -78,6 +85,8 @@ class View extends \Magento\Framework\App\Helper\AbstractHelper
* @param \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator
* @param array $messageGroups
* @param \Magento\Framework\Stdlib\StringUtils|null $string
+ * @param LayoutUpdateManager|null $layoutUpdateManager
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Framework\App\Helper\Context $context,
@@ -88,7 +97,8 @@ public function __construct(
\Magento\Framework\Message\ManagerInterface $messageManager,
\Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator $categoryUrlPathGenerator,
array $messageGroups = [],
- \Magento\Framework\Stdlib\StringUtils $string = null
+ \Magento\Framework\Stdlib\StringUtils $string = null,
+ ?LayoutUpdateManager $layoutUpdateManager = null
) {
$this->_catalogSession = $catalogSession;
$this->_catalogDesign = $catalogDesign;
@@ -97,8 +107,9 @@ public function __construct(
$this->messageGroups = $messageGroups;
$this->messageManager = $messageManager;
$this->categoryUrlPathGenerator = $categoryUrlPathGenerator;
- $this->string = $string ?: \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Framework\Stdlib\StringUtils::class);
+ $this->string = $string ?: ObjectManager::getInstance()->get(\Magento\Framework\Stdlib\StringUtils::class);
+ $this->layoutUpdateManager = $layoutUpdateManager
+ ?? ObjectManager::getInstance()->get(LayoutUpdateManager::class);
parent::__construct($context);
}
@@ -203,6 +214,9 @@ public function initProductLayout(ResultPage $resultPage, $product, $params = nu
}
}
}
+ if ($settings->getPageLayoutHandles()) {
+ $resultPage->addPageLayoutHandles($settings->getPageLayoutHandles());
+ }
$currentCategory = $this->_coreRegistry->registry('current_category');
$controllerClass = $this->_request->getFullActionName();
diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php
new file mode 100644
index 0000000000000..1aa7ab7e5880f
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdate.php
@@ -0,0 +1,119 @@
+getAttribute()->getAttributeCode();
+
+ return $model->getData($code);
+ }
+
+ /**
+ * Compose list of available files (layout handles) for given entity.
+ *
+ * @param AbstractModel $forModel
+ * @return string[]
+ */
+ abstract protected function listAvailableValues(AbstractModel $forModel): array;
+
+ /**
+ * Extracts prepare attribute value to be saved.
+ *
+ * @throws LocalizedException
+ * @param AbstractModel $model
+ * @return string|null
+ */
+ private function prepareValue(AbstractModel $model): ?string
+ {
+ $value = $this->extractAttributeValue($model);
+ if (!is_string($value)) {
+ $value = null;
+ }
+ if ($value
+ && $value !== self::VALUE_USE_UPDATE_XML
+ && $value !== self::VALUE_NO_UPDATE
+ && !in_array($value, $this->listAvailableValues($model), true)
+ ) {
+ throw new LocalizedException(__('Selected layout update is not available'));
+ }
+
+ return $value;
+ }
+
+ /**
+ * Set value for the object.
+ *
+ * @param string|null $value
+ * @param AbstractModel $forObject
+ * @param string|null $attrCode
+ * @return void
+ */
+ private function setAttributeValue(?string $value, AbstractModel $forObject, ?string $attrCode = null): void
+ {
+ $attrCode = $attrCode ?? $this->getAttribute()->getAttributeCode();
+ if ($forObject->hasData(AbstractModel::CUSTOM_ATTRIBUTES)) {
+ $forObject->setCustomAttribute($attrCode, $value);
+ }
+ $forObject->setData($attrCode, $value);
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param AbstractModel $object
+ */
+ public function validate($object)
+ {
+ $valid = parent::validate($object);
+ if ($valid) {
+ $this->prepareValue($object);
+ }
+
+ return $valid;
+ }
+
+ /**
+ * @inheritDoc
+ * @param AbstractModel $object
+ * @throws LocalizedException
+ */
+ public function beforeSave($object)
+ {
+ $value = $this->prepareValue($object);
+ if ($value && ($value === self::VALUE_NO_UPDATE || $value !== self::VALUE_USE_UPDATE_XML)) {
+ $this->setAttributeValue(null, $object, 'custom_layout_update');
+ }
+ if (!$value || $value === self::VALUE_USE_UPDATE_XML || $value === self::VALUE_NO_UPDATE) {
+ $value = null;
+ }
+ $this->setAttributeValue($value, $object);
+
+ return $this;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php
index a994446881189..b5aa5e2035100 100644
--- a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php
+++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php
@@ -5,8 +5,10 @@
*/
namespace Magento\Catalog\Model\Attribute\Backend;
+use Magento\Catalog\Model\AbstractModel;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\View\Model\Layout\Update\ValidatorFactory;
-use Magento\Eav\Model\Entity\Attribute\Exception;
+use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend;
/**
* Layout update attribute backend
@@ -16,18 +18,15 @@
* @SuppressWarnings(PHPMD.LongVariable)
* @since 100.0.2
*/
-class Customlayoutupdate extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
+class Customlayoutupdate extends AbstractBackend
{
/**
- * Layout update validator factory
- *
* @var ValidatorFactory
+ * @deprecated Is not used anymore.
*/
protected $_layoutUpdateValidatorFactory;
/**
- * Construct the custom layout update class
- *
* @param ValidatorFactory $layoutUpdateValidatorFactory
*/
public function __construct(ValidatorFactory $layoutUpdateValidatorFactory)
@@ -36,31 +35,95 @@ public function __construct(ValidatorFactory $layoutUpdateValidatorFactory)
}
/**
- * Validate the custom layout update
+ * Extract an attribute value.
*
- * @param \Magento\Framework\DataObject $object
- * @return bool
- * @throws Exception
+ * @param AbstractModel $object
+ * @return mixed
*/
- public function validate($object)
+ private function extractValue(AbstractModel $object)
+ {
+ $attributeCode = $attributeCode ?? $this->getAttribute()->getName();
+ $value = $object->getData($attributeCode);
+ if (!$value || !is_string($value)) {
+ $value = null;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Extract old attribute value.
+ *
+ * @param AbstractModel $object
+ * @return mixed Old value or null.
+ */
+ private function extractOldValue(AbstractModel $object)
{
- $attributeName = $this->getAttribute()->getName();
- $xml = trim($object->getData($attributeName));
+ if (!empty($object->getId())) {
+ $attr = $this->getAttribute()->getAttributeCode();
+
+ if ($object->getOrigData()) {
+ return $object->getOrigData($attr);
+ }
- if (!$this->getAttribute()->getIsRequired() && empty($xml)) {
- return true;
+ $oldObject = clone $object;
+ $oldObject->unsetData();
+ $oldObject->load($object->getId());
+
+ return $oldObject->getData($attr);
}
- /** @var $validator \Magento\Framework\View\Model\Layout\Update\Validator */
- $validator = $this->_layoutUpdateValidatorFactory->create();
- if (!$validator->isValid($xml)) {
- $messages = $validator->getMessages();
- //Add first message to exception
- $message = array_shift($messages);
- $eavExc = new Exception(__($message));
- $eavExc->setAttributeCode($attributeName);
- throw $eavExc;
+ return null;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param AbstractModel $object
+ */
+ public function validate($object)
+ {
+ if (parent::validate($object)) {
+ if ($object instanceof AbstractModel) {
+ $value = $this->extractValue($object);
+ $oldValue = $this->extractOldValue($object);
+ if ($value && $oldValue !== $value) {
+ throw new LocalizedException(__('Custom layout update text cannot be changed, only removed'));
+ }
+ }
}
+
return true;
}
+
+ /**
+ * Put an attribute value.
+ *
+ * @param AbstractModel $object
+ * @param string|null $value
+ * @return void
+ */
+ private function putValue(AbstractModel $object, ?string $value): void
+ {
+ $attributeCode = $this->getAttribute()->getName();
+ if ($object->hasData(AbstractModel::CUSTOM_ATTRIBUTES)) {
+ $object->setCustomAttribute($attributeCode, $value);
+ }
+ $object->setData($attributeCode, $value);
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param AbstractModel $object
+ * @throws LocalizedException
+ */
+ public function beforeSave($object)
+ {
+ //Validate first, validation might have been skipped.
+ $this->validate($object);
+ $this->putValue($object, $this->extractValue($object));
+
+ return parent::beforeSave($object);
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php
index 0940ca7a234a3..cf194615b1f3b 100644
--- a/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php
+++ b/app/code/Magento/Catalog/Model/Attribute/ScopeOverriddenValue.php
@@ -81,7 +81,7 @@ public function containsValue($entityType, $entity, $attributeCode, $storeId)
if ((int)$storeId === Store::DEFAULT_STORE_ID) {
return false;
}
- if ($this->attributesValues === null) {
+ if (!isset($this->attributesValues[$storeId])) {
$this->initAttributeValues($entityType, $entity, (int)$storeId);
}
@@ -110,6 +110,8 @@ public function getDefaultValues($entityType, $entity)
}
/**
+ * Init attribute values.
+ *
* @param string $entityType
* @param \Magento\Catalog\Model\AbstractModel $entity
* @param int $storeId
@@ -158,6 +160,8 @@ private function initAttributeValues($entityType, $entity, $storeId)
}
/**
+ * Returns entity attributes.
+ *
* @param string $entityType
* @return \Magento\Eav\Api\Data\AttributeInterface[]
*/
diff --git a/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php b/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php
new file mode 100644
index 0000000000000..0003b9996c84b
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Attribute/Source/AbstractLayoutUpdate.php
@@ -0,0 +1,101 @@
+optionsText[$default] = $defaultText;
+
+ return [['label' => $defaultText, 'value' => $default]];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getOptionText($value)
+ {
+ if (is_scalar($value) && array_key_exists($value, $this->optionsText)) {
+ return $this->optionsText[$value];
+ }
+
+ return false;
+ }
+
+ /**
+ * Extract attribute value.
+ *
+ * @param CustomAttributesDataInterface|AbstractExtensibleModel $entity
+ * @return mixed
+ */
+ private function extractAttributeValue(CustomAttributesDataInterface $entity)
+ {
+ $attrCode = 'custom_layout_update';
+ if ($entity instanceof AbstractExtensibleModel
+ && !$entity->hasData(CustomAttributesDataInterface::CUSTOM_ATTRIBUTES)
+ ) {
+ //Custom attributes were not loaded yet, using data array
+ return $entity->getData($attrCode);
+ }
+ //Fallback to customAttribute method
+ $attr = $entity->getCustomAttribute($attrCode);
+
+ return $attr ? $attr->getValue() : null;
+ }
+
+ /**
+ * List available layout update options for the entity.
+ *
+ * @param CustomAttributesDataInterface $entity
+ * @return string[]
+ */
+ abstract protected function listAvailableOptions(CustomAttributesDataInterface $entity): array;
+
+ /**
+ * @inheritDoc
+ *
+ * @param CustomAttributesDataInterface|AbstractExtensibleModel $entity
+ */
+ public function getOptionsFor(CustomAttributesDataInterface $entity): array
+ {
+ $options = $this->getAllOptions();
+ if ($this->extractAttributeValue($entity)) {
+ $existingValue = Backend::VALUE_USE_UPDATE_XML;
+ $existingLabel = 'Use existing';
+ $options[] = ['label' => $existingLabel, 'value' => $existingValue];
+ $this->optionsText[$existingValue] = $existingLabel;
+ }
+ foreach ($this->listAvailableOptions($entity) as $handle) {
+ $options[] = ['label' => $handle, 'value' => $handle];
+ $this->optionsText[$handle] = $handle;
+ }
+
+ return $options;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php
index 4ddfd1f3b63a8..330debdc32469 100644
--- a/app/code/Magento/Catalog/Model/Category.php
+++ b/app/code/Magento/Catalog/Model/Category.php
@@ -5,13 +5,10 @@
*/
namespace Magento\Catalog\Model;
-use Magento\Authorization\Model\UserContextInterface;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator;
use Magento\Framework\Api\AttributeValueFactory;
-use Magento\Framework\App\ObjectManager;
-use Magento\Framework\AuthorizationInterface;
use Magento\Framework\Convert\ConvertArray;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Profiler;
@@ -131,7 +128,8 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements
'page_layout',
'custom_layout_update',
'custom_apply_to_products',
- 'custom_use_parent_settings',
+ 'custom_layout_update_file',
+ 'custom_use_parent_settings'
];
/**
@@ -215,16 +213,6 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements
*/
protected $metadataService;
- /**
- * @var UserContextInterface
- */
- private $userContext;
-
- /**
- * @var AuthorizationInterface
- */
- private $authorization;
-
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -495,7 +483,7 @@ public function getProductCollection()
* Retrieve all customer attributes
*
* @param bool $noDesignAttributes
- * @return array
+ * @return \Magento\Eav\Api\Data\AttributeInterface[]
* @todo Use with Flat Resource
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
*/
@@ -764,7 +752,7 @@ public function getCustomDesignDate()
/**
* Retrieve design attributes array
*
- * @return array
+ * @return \Magento\Eav\Api\Data\AttributeInterface[]
*/
public function getDesignAttributes()
{
@@ -936,60 +924,6 @@ public function beforeDelete()
return parent::beforeDelete();
}
- /**
- * Get user context.
- *
- * @return UserContextInterface
- */
- private function getUserContext(): UserContextInterface
- {
- if (!$this->userContext) {
- $this->userContext = ObjectManager::getInstance()->get(UserContextInterface::class);
- }
-
- return $this->userContext;
- }
-
- /**
- * Get authorization service.
- *
- * @return AuthorizationInterface
- */
- private function getAuthorization(): AuthorizationInterface
- {
- if (!$this->authorization) {
- $this->authorization = ObjectManager::getInstance()->get(AuthorizationInterface::class);
- }
-
- return $this->authorization;
- }
-
- /**
- * @inheritDoc
- */
- public function beforeSave()
- {
- //Validate changing of design.
- $userType = $this->getUserContext()->getUserType();
- if ((
- $userType === UserContextInterface::USER_TYPE_ADMIN
- || $userType === UserContextInterface::USER_TYPE_INTEGRATION
- )
- && !$this->getAuthorization()->isAllowed('Magento_Catalog::edit_category_design')
- ) {
- foreach ($this->_designAttributes as $attributeCode) {
- $this->setData($attributeCode, $value = $this->getOrigData($attributeCode));
- if (!empty($this->_data[self::CUSTOM_ATTRIBUTES])
- && array_key_exists($attributeCode, $this->_data[self::CUSTOM_ATTRIBUTES])) {
- //In case custom attribute were used to update the entity.
- $this->_data[self::CUSTOM_ATTRIBUTES][$attributeCode]->setValue($value);
- }
- }
- }
-
- return parent::beforeSave();
- }
-
/**
* Retrieve anchors above
*
@@ -1201,8 +1135,6 @@ public function reindex()
|| $this->dataHasChangedFor('is_active')) {
if (!$productIndexer->isScheduled()) {
$productIndexer->reindexList($this->getPathIds());
- } else {
- $productIndexer->invalidate();
}
}
}
@@ -1360,6 +1292,7 @@ public function getChildrenData()
//@codeCoverageIgnoreEnd
+ // phpcs:disable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames
/**
* Return Data Object data in array format.
*
@@ -1368,6 +1301,7 @@ public function getChildrenData()
*/
public function __toArray()
{
+ // phpcs:enable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames
$data = $this->_data;
$hasToArray = function ($model) {
return is_object($model) && method_exists($model, '__toArray') && is_callable([$model, '__toArray']);
diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php
index 4880214e5c6a6..865160da14a6f 100644
--- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php
+++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Image.php
@@ -5,8 +5,12 @@
*/
namespace Magento\Catalog\Model\Category\Attribute\Backend;
+use Magento\Catalog\Model\ImageUploader;
use Magento\Framework\App\Filesystem\DirectoryList;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\File\Uploader;
+use Magento\Store\Api\Data\StoreInterface;
+use Magento\Store\Model\StoreManagerInterface;
/**
* Catalog category image attribute backend model
@@ -45,7 +49,7 @@ class Image extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
protected $_logger;
/**
- * @var \Magento\Catalog\Model\ImageUploader
+ * @var ImageUploader
*/
private $imageUploader;
@@ -54,19 +58,32 @@ class Image extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
*/
private $additionalData = '_additional_data_';
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
/**
* @param \Psr\Log\LoggerInterface $logger
* @param \Magento\Framework\Filesystem $filesystem
* @param \Magento\MediaStorage\Model\File\UploaderFactory $fileUploaderFactory
+ * @param StoreManagerInterface $storeManager
+ * @param ImageUploader $imageUploader
*/
public function __construct(
\Psr\Log\LoggerInterface $logger,
\Magento\Framework\Filesystem $filesystem,
- \Magento\MediaStorage\Model\File\UploaderFactory $fileUploaderFactory
+ \Magento\MediaStorage\Model\File\UploaderFactory $fileUploaderFactory,
+ StoreManagerInterface $storeManager = null,
+ ImageUploader $imageUploader = null
) {
$this->_filesystem = $filesystem;
$this->_fileUploaderFactory = $fileUploaderFactory;
$this->_logger = $logger;
+ $this->storeManager = $storeManager ??
+ ObjectManager::getInstance()->get(StoreManagerInterface::class);
+ $this->imageUploader = $imageUploader ??
+ ObjectManager::getInstance()->get(ImageUploader::class);
}
/**
@@ -94,13 +111,13 @@ private function getUploadedImageName($value)
*/
private function checkUniqueImageName(string $imageName): string
{
- $imageUploader = $this->getImageUploader();
$mediaDirectory = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA);
$imageAbsolutePath = $mediaDirectory->getAbsolutePath(
- $imageUploader->getBasePath() . DIRECTORY_SEPARATOR . $imageName
+ $this->imageUploader->getBasePath() . DIRECTORY_SEPARATOR . $imageName
);
- $imageName = Uploader::getNewFilename($imageAbsolutePath);
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ $imageName = call_user_func([Uploader::class, 'getNewFilename'], $imageAbsolutePath);
return $imageName;
}
@@ -119,7 +136,18 @@ public function beforeSave($object)
$attributeName = $this->getAttribute()->getName();
$value = $object->getData($attributeName);
- if ($this->fileResidesOutsideCategoryDir($value)) {
+ if ($this->isTmpFileAvailable($value) && $imageName = $this->getUploadedImageName($value)) {
+ try {
+ /** @var StoreInterface $store */
+ $store = $this->storeManager->getStore();
+ $baseMediaDir = $store->getBaseMediaDir();
+ $newImgRelativePath = $this->imageUploader->moveFileFromTmp($imageName, true);
+ $value[0]['url'] = '/' . $baseMediaDir . '/' . $newImgRelativePath;
+ $value[0]['name'] = $value[0]['url'];
+ } catch (\Exception $e) {
+ $this->_logger->critical($e);
+ }
+ } elseif ($this->fileResidesOutsideCategoryDir($value)) {
// use relative path for image attribute so we know it's outside of category dir when we fetch it
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$value[0]['url'] = parse_url($value[0]['url'], PHP_URL_PATH);
@@ -139,23 +167,6 @@ public function beforeSave($object)
return parent::beforeSave($object);
}
- /**
- * Get Instance of Category Image Uploader.
- *
- * @return \Magento\Catalog\Model\ImageUploader
- *
- * @deprecated 101.0.0
- */
- private function getImageUploader()
- {
- if ($this->imageUploader === null) {
- $this->imageUploader = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Catalog\CategoryImageUpload::class);
- }
-
- return $this->imageUploader;
- }
-
/**
* Check if temporary file is available for new image upload.
*
@@ -194,19 +205,10 @@ private function fileResidesOutsideCategoryDir($value)
*
* @param \Magento\Framework\DataObject $object
* @return \Magento\Catalog\Model\Category\Attribute\Backend\Image
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterSave($object)
{
- $value = $object->getData($this->additionalData . $this->getAttribute()->getName());
-
- if ($this->isTmpFileAvailable($value) && $imageName = $this->getUploadedImageName($value)) {
- try {
- $this->getImageUploader()->moveFileFromTmp($imageName);
- } catch (\Exception $e) {
- $this->_logger->critical($e);
- }
- }
-
return $this;
}
}
diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php
new file mode 100644
index 0000000000000..215fe1c19bd8d
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/LayoutUpdate.php
@@ -0,0 +1,44 @@
+manager = $manager;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param AbstractModel|Category $forModel
+ */
+ protected function listAvailableValues(AbstractModel $forModel): array
+ {
+ return $this->manager->fetchAvailableFiles($forModel);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php b/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php
new file mode 100644
index 0000000000000..f5694a46d3fb2
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Category/Attribute/LayoutUpdateManager.php
@@ -0,0 +1,154 @@
+themeFactory = $themeFactory;
+ $this->design = $design;
+ $this->layoutProcessorFactory = $layoutProcessorFactory;
+ }
+
+ /**
+ * Get the processor instance.
+ *
+ * @return LayoutProcessor
+ */
+ private function getLayoutProcessor(): LayoutProcessor
+ {
+ if (!$this->layoutProcessor) {
+ $this->layoutProcessor = $this->layoutProcessorFactory->create(
+ [
+ 'theme' => $this->themeFactory->create(
+ $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND)
+ )
+ ]
+ );
+ $this->themeFactory = null;
+ $this->design = null;
+ }
+
+ return $this->layoutProcessor;
+ }
+
+ /**
+ * Fetch list of available files/handles for the category.
+ *
+ * @param CategoryInterface $category
+ * @return string[]
+ */
+ public function fetchAvailableFiles(CategoryInterface $category): array
+ {
+ if (!$category->getId()) {
+ return [];
+ }
+
+ $handles = $this->getLayoutProcessor()->getAvailableHandles();
+
+ return array_filter(
+ array_map(
+ function (string $handle) use ($category) : ?string {
+ preg_match(
+ '/^catalog\_category\_view\_selectable\_' .$category->getId() .'\_([a-z0-9]+)/i',
+ $handle,
+ $selectable
+ );
+ if (!empty($selectable[1])) {
+ return $selectable[1];
+ }
+
+ return null;
+ },
+ $handles
+ )
+ );
+ }
+
+ /**
+ * Extract custom layout attribute value.
+ *
+ * @param CategoryInterface $category
+ * @return mixed
+ */
+ private function extractAttributeValue(CategoryInterface $category)
+ {
+ if ($category instanceof Category && !$category->hasData(CategoryInterface::CUSTOM_ATTRIBUTES)) {
+ return $category->getData('custom_layout_update_file');
+ }
+ if ($attr = $category->getCustomAttribute('custom_layout_update_file')) {
+ return $attr->getValue();
+ }
+
+ return null;
+ }
+
+ /**
+ * Extract selected custom layout settings.
+ *
+ * If no update is selected none will apply.
+ *
+ * @param CategoryInterface $category
+ * @param DataObject $intoSettings
+ * @return void
+ */
+ public function extractCustomSettings(CategoryInterface $category, DataObject $intoSettings): void
+ {
+ if ($category->getId() && $value = $this->extractAttributeValue($category)) {
+ $handles = $intoSettings->getPageLayoutHandles() ?? [];
+ $handles = array_merge_recursive(
+ $handles,
+ ['selectable' => $category->getId() . '_' . $value]
+ );
+ $intoSettings->setPageLayoutHandles($handles);
+ }
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php
new file mode 100644
index 0000000000000..1c307220aa9f8
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/LayoutUpdate.php
@@ -0,0 +1,40 @@
+manager = $manager;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function listAvailableOptions(CustomAttributesDataInterface $entity): array
+ {
+ return $this->manager->fetchAvailableFiles($entity);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Category/Authorization.php b/app/code/Magento/Catalog/Model/Category/Authorization.php
new file mode 100644
index 0000000000000..629a3c2319472
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Category/Authorization.php
@@ -0,0 +1,158 @@
+authorization = $authorization;
+ $this->categoryFactory = $factory;
+ }
+
+ /**
+ * Extract attribute value from the model.
+ *
+ * @param CategoryModel $category
+ * @param AttributeInterface $attr
+ * @throws \RuntimeException When no new value is present.
+ * @return mixed
+ */
+ private function extractAttributeValue(CategoryModel $category, AttributeInterface $attr)
+ {
+ if ($category->hasData($attr->getAttributeCode())) {
+ $newValue = $category->getData($attr->getAttributeCode());
+ } elseif ($category->hasData(CategoryModel::CUSTOM_ATTRIBUTES)
+ && $attrValue = $category->getCustomAttribute($attr->getAttributeCode())
+ ) {
+ $newValue = $attrValue->getValue();
+ } else {
+ throw new \RuntimeException('New value is not set');
+ }
+
+ if (empty($newValue)
+ || ($attr->getBackend() instanceof LayoutUpdate
+ && ($newValue === LayoutUpdate::VALUE_USE_UPDATE_XML || $newValue === LayoutUpdate::VALUE_NO_UPDATE)
+ )
+ ) {
+ $newValue = null;
+ }
+
+ return $newValue;
+ }
+
+ /**
+ * Find values to compare the new one.
+ *
+ * @param AttributeInterface $attribute
+ * @param array|null $oldCategory
+ * @return mixed[]
+ */
+ private function fetchOldValue(AttributeInterface $attribute, ?array $oldCategory): array
+ {
+ $oldValues = [null];
+ $attrCode = $attribute->getAttributeCode();
+ if ($oldCategory) {
+ //New value must match saved value exactly
+ $oldValues = [!empty($oldCategory[$attrCode]) ? $oldCategory[$attrCode] : null];
+ if (empty($oldValues[0])) {
+ $oldValues[0] = null;
+ }
+ } else {
+ //New value can be either empty or default value.
+ $oldValues[] = $attribute->getDefaultValue();
+ }
+
+ return $oldValues;
+ }
+
+ /**
+ * Determine whether a category has design properties changed.
+ *
+ * @param CategoryModel $category
+ * @param array|null $oldCategory
+ * @return bool
+ */
+ private function hasChanges(CategoryModel $category, ?array $oldCategory): bool
+ {
+ foreach ($category->getDesignAttributes() as $designAttribute) {
+ $oldValues = $this->fetchOldValue($designAttribute, $oldCategory);
+ try {
+ $newValue = $this->extractAttributeValue($category, $designAttribute);
+ } catch (\RuntimeException $exception) {
+ //No new value
+ continue;
+ }
+
+ if (!in_array($newValue, $oldValues, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Authorize saving of a category.
+ *
+ * @throws AuthorizationException
+ * @throws NoSuchEntityException When a category with invalid ID given.
+ * @param CategoryInterface|CategoryModel $category
+ * @return void
+ */
+ public function authorizeSavingOf(CategoryInterface $category): void
+ {
+ if (!$this->authorization->isAllowed('Magento_Catalog::edit_category_design')) {
+ $oldData = null;
+ if ($category->getId()) {
+ if ($category->getOrigData()) {
+ $oldData = $category->getOrigData();
+ } else {
+ /** @var CategoryModel $savedCategory */
+ $savedCategory = $this->categoryFactory->create();
+ $savedCategory->load($category->getId());
+ if (!$savedCategory->getName()) {
+ throw NoSuchEntityException::singleField('id', $category->getId());
+ }
+ $oldData = $savedCategory->getData();
+ }
+ }
+
+ if ($this->hasChanges($category, $oldData)) {
+ throw new AuthorizationException(__('Not allowed to edit the category\'s design attributes'));
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php
index d2e237779e2a8..fe7258398d191 100644
--- a/app/code/Magento/Catalog/Model/Category/DataProvider.php
+++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php
@@ -12,15 +12,19 @@
use Magento\Catalog\Model\Attribute\ScopeOverriddenValue;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\Category\Attribute\Backend\Image as ImageBackendModel;
+use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate;
use Magento\Catalog\Model\CategoryFactory;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute;
use Magento\Eav\Api\Data\AttributeInterface;
use Magento\Eav\Model\Config;
+use Magento\Eav\Model\Entity\Attribute\Source\SpecificSourceInterface;
use Magento\Eav\Model\Entity\Type;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\App\RequestInterface;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
-use Magento\Framework\Filesystem;
+use Magento\Framework\Registry;
use Magento\Framework\Stdlib\ArrayManager;
use Magento\Framework\Stdlib\ArrayUtils;
use Magento\Store\Model\Store;
@@ -29,6 +33,7 @@
use Magento\Ui\DataProvider\EavValidationRules;
use Magento\Ui\DataProvider\Modifier\PoolInterface;
use Magento\Framework\AuthorizationInterface;
+use Magento\Ui\DataProvider\ModifierPoolDataProvider;
/**
* Category form data provider.
@@ -38,7 +43,7 @@
* @SuppressWarnings(PHPMD.TooManyFields)
* @since 101.0.0
*/
-class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider
+class DataProvider extends ModifierPoolDataProvider
{
/**
* @var string
@@ -69,6 +74,8 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider
'size' => 'multiline_count',
];
+ private $boolMetaProperties = ['visible', 'required'];
+
/**
* Form element mapping
*
@@ -103,6 +110,15 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider
'position'
];
+ /**
+ * Elements with currency symbol
+ *
+ * @var array
+ */
+ private $elementsWithCurrencySymbol = [
+ 'filter_price_range',
+ ];
+
/**
* @var EavValidationRules
* @since 101.0.0
@@ -110,13 +126,13 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider
protected $eavValidationRules;
/**
- * @var \Magento\Framework\Registry
+ * @var Registry
* @since 101.0.0
*/
protected $registry;
/**
- * @var \Magento\Framework\App\RequestInterface
+ * @var RequestInterface
* @since 101.0.0
*/
protected $request;
@@ -152,7 +168,7 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider
private $arrayUtils;
/**
- * @var Filesystem
+ * @var FileInfo
*/
private $fileInfo;
@@ -168,16 +184,18 @@ class DataProvider extends \Magento\Ui\DataProvider\ModifierPoolDataProvider
* @param EavValidationRules $eavValidationRules
* @param CategoryCollectionFactory $categoryCollectionFactory
* @param StoreManagerInterface $storeManager
- * @param \Magento\Framework\Registry $registry
+ * @param Registry $registry
* @param Config $eavConfig
- * @param \Magento\Framework\App\RequestInterface $request
+ * @param RequestInterface $request
* @param CategoryFactory $categoryFactory
* @param array $meta
* @param array $data
* @param PoolInterface|null $pool
* @param AuthorizationInterface|null $auth
* @param ArrayUtils|null $arrayUtils
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @param ScopeOverriddenValue|null $scopeOverriddenValue
+ * @param ArrayManager|null $arrayManager
+ * @param FileInfo|null $fileInfo
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -187,15 +205,18 @@ public function __construct(
EavValidationRules $eavValidationRules,
CategoryCollectionFactory $categoryCollectionFactory,
StoreManagerInterface $storeManager,
- \Magento\Framework\Registry $registry,
+ Registry $registry,
Config $eavConfig,
- \Magento\Framework\App\RequestInterface $request,
+ RequestInterface $request,
CategoryFactory $categoryFactory,
array $meta = [],
array $data = [],
PoolInterface $pool = null,
?AuthorizationInterface $auth = null,
- ?ArrayUtils $arrayUtils = null
+ ?ArrayUtils $arrayUtils = null,
+ ScopeOverriddenValue $scopeOverriddenValue = null,
+ ArrayManager $arrayManager = null,
+ FileInfo $fileInfo = null
) {
$this->eavValidationRules = $eavValidationRules;
$this->collection = $categoryCollectionFactory->create();
@@ -207,6 +228,10 @@ public function __construct(
$this->categoryFactory = $categoryFactory;
$this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class);
$this->arrayUtils = $arrayUtils ?? ObjectManager::getInstance()->get(ArrayUtils::class);
+ $this->scopeOverriddenValue = $scopeOverriddenValue ?:
+ ObjectManager::getInstance()->get(ScopeOverriddenValue::class);
+ $this->arrayManager = $arrayManager ?: ObjectManager::getInstance()->get(ArrayManager::class);
+ $this->fileInfo = $fileInfo ?: ObjectManager::getInstance()->get(FileInfo::class);
parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data, $pool);
}
@@ -244,7 +269,7 @@ private function addUseDefaultValueCheckbox(Category $category, array $meta): ar
$canDisplayUseDefault = $attribute->getScope() != EavAttributeInterface::SCOPE_GLOBAL_TEXT
&& $category->getId()
&& $category->getStoreId();
- $attributePath = $this->getArrayManager()->findPath($attributeCode, $meta);
+ $attributePath = $this->arrayManager->findPath($attributeCode, $meta);
if (!$attributePath
|| !$canDisplayUseDefault
@@ -253,14 +278,14 @@ private function addUseDefaultValueCheckbox(Category $category, array $meta): ar
continue;
}
- $meta = $this->getArrayManager()->merge(
+ $meta = $this->arrayManager->merge(
[$attributePath, 'arguments/data/config'],
$meta,
[
'service' => [
'template' => 'ui/form/element/helper/service',
],
- 'disabled' => !$this->getScopeOverriddenValue()->containsValue(
+ 'disabled' => !$this->scopeOverriddenValue->containsValue(
CategoryInterface::class,
$category,
$attributeCode,
@@ -351,7 +376,7 @@ public function getData()
*
* @param Type $entityType
* @return array
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @since 101.0.0
@@ -369,16 +394,26 @@ public function getAttributesMeta(Type $entityType)
foreach ($this->metaProperties as $metaName => $origName) {
$value = $attribute->getDataUsingMethod($origName);
$meta[$code][$metaName] = $value;
+ if (in_array($metaName, $this->boolMetaProperties, true)) {
+ $meta[$code][$metaName] = (bool)$meta[$code][$metaName];
+ }
if ('frontend_input' === $origName) {
$meta[$code]['formElement'] = isset($this->formElement[$value])
? $this->formElement[$value]
: $value;
}
if ($attribute->usesSource()) {
- $meta[$code]['options'] = $attribute->getSource()->getAllOptions();
- foreach ($meta[$code]['options'] as &$option) {
+ $source = $attribute->getSource();
+ $currentCategory = $this->getCurrentCategory();
+ if ($source instanceof SpecificSourceInterface && $currentCategory) {
+ $options = $source->getOptionsFor($currentCategory);
+ } else {
+ $options = $source->getAllOptions();
+ }
+ foreach ($options as &$option) {
$option['__disableTmpl'] = true;
}
+ $meta[$code]['options'] = $options;
}
}
@@ -394,11 +429,22 @@ public function getAttributesMeta(Type $entityType)
if ($category) {
$attributeIsLocked = $category->isLockedAttribute($code);
$meta[$code]['disabled'] = $attributeIsLocked;
- $hasUseConfigField = (bool) array_search('use_config.' . $code, $fields, true);
+ $hasUseConfigField = (bool)array_search('use_config.' . $code, $fields, true);
if ($hasUseConfigField && $meta[$code]['disabled']) {
$meta['use_config.' . $code]['disabled'] = true;
}
}
+
+ if (in_array($code, $this->elementsWithCurrencySymbol, false)) {
+ $requestScope = $this->request->getParam(
+ $this->requestScopeFieldName,
+ Store::DEFAULT_STORE_ID
+ );
+
+ $meta[$code]['addbefore'] = $this->storeManager->getStore($requestScope)
+ ->getBaseCurrency()
+ ->getCurrencySymbol();
+ }
}
$result = [];
@@ -438,7 +484,7 @@ protected function addUseConfigSettings($categoryData)
/**
* Add use default settings
*
- * @param \Magento\Catalog\Model\Category $category
+ * @param Category $category
* @param array $categoryData
* @return array
* @deprecated 101.1.0
@@ -526,13 +572,19 @@ protected function filterFields($categoryData)
/**
* Converts category image data to acceptable for rendering format
*
- * @param \Magento\Catalog\Model\Category $category
+ * @param Category $category
* @param array $categoryData
* @return array
*/
private function convertValues($category, $categoryData): array
{
foreach ($category->getAttributes() as $attributeCode => $attribute) {
+ if ($attributeCode === 'custom_layout_update_file') {
+ if (!empty($categoryData['custom_layout_update'])) {
+ $categoryData['custom_layout_update_file']
+ = LayoutUpdate::VALUE_USE_UPDATE_XML;
+ }
+ }
if (!isset($categoryData[$attributeCode])) {
continue;
}
@@ -541,16 +593,15 @@ private function convertValues($category, $categoryData): array
unset($categoryData[$attributeCode]);
$fileName = $category->getData($attributeCode);
- $fileInfo = $this->getFileInfo();
- if ($fileInfo->isExist($fileName)) {
- $stat = $fileInfo->getStat($fileName);
- $mime = $fileInfo->getMimeType($fileName);
+ if ($this->fileInfo->isExist($fileName)) {
+ $stat = $this->fileInfo->getStat($fileName);
+ $mime = $this->fileInfo->getMimeType($fileName);
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$categoryData[$attributeCode][0]['name'] = basename($fileName);
- if ($fileInfo->isBeginsWithMediaDirectoryPath($fileName)) {
+ if ($this->fileInfo->isBeginsWithMediaDirectoryPath($fileName)) {
$categoryData[$attributeCode][0]['url'] = $fileName;
} else {
$categoryData[$attributeCode][0]['url'] = $category->getImageUrl($attributeCode);
@@ -592,52 +643,53 @@ protected function getFieldsMap()
{
return [
'general' => [
- 'parent',
- 'path',
- 'is_active',
- 'include_in_menu',
- 'name',
- ],
+ 'parent',
+ 'path',
+ 'is_active',
+ 'include_in_menu',
+ 'name',
+ ],
'content' => [
- 'image',
- 'description',
- 'landing_page',
- ],
+ 'image',
+ 'description',
+ 'landing_page',
+ ],
'display_settings' => [
- 'display_mode',
- 'is_anchor',
- 'available_sort_by',
- 'use_config.available_sort_by',
- 'default_sort_by',
- 'use_config.default_sort_by',
- 'filter_price_range',
- 'use_config.filter_price_range',
- ],
+ 'display_mode',
+ 'is_anchor',
+ 'available_sort_by',
+ 'use_config.available_sort_by',
+ 'default_sort_by',
+ 'use_config.default_sort_by',
+ 'filter_price_range',
+ 'use_config.filter_price_range',
+ ],
'search_engine_optimization' => [
- 'url_key',
- 'url_key_create_redirect',
- 'url_key_group',
- 'meta_title',
- 'meta_keywords',
- 'meta_description',
- ],
+ 'url_key',
+ 'url_key_create_redirect',
+ 'url_key_group',
+ 'meta_title',
+ 'meta_keywords',
+ 'meta_description',
+ ],
'assign_products' => [
- ],
+ ],
'design' => [
- 'custom_use_parent_settings',
- 'custom_apply_to_products',
- 'custom_design',
- 'page_layout',
- 'custom_layout_update',
- ],
+ 'custom_use_parent_settings',
+ 'custom_apply_to_products',
+ 'custom_design',
+ 'page_layout',
+ 'custom_layout_update',
+ 'custom_layout_update_file'
+ ],
'schedule_design_update' => [
- 'custom_design_from',
- 'custom_design_to',
- ],
+ 'custom_design_from',
+ 'custom_design_to',
+ ],
'category_view_optimization' => [
- ],
+ ],
'category_permissions' => [
- ],
+ ],
];
}
@@ -651,53 +703,4 @@ private function getFields(): array
$fieldsMap = $this->getFieldsMap();
return $this->arrayUtils->flatten($fieldsMap);
}
-
- /**
- * Retrieve scope overridden value
- *
- * @return ScopeOverriddenValue
- * @deprecated 101.1.0
- */
- private function getScopeOverriddenValue(): ScopeOverriddenValue
- {
- if (null === $this->scopeOverriddenValue) {
- $this->scopeOverriddenValue = \Magento\Framework\App\ObjectManager::getInstance()->get(
- ScopeOverriddenValue::class
- );
- }
-
- return $this->scopeOverriddenValue;
- }
-
- /**
- * Retrieve array manager
- *
- * @return ArrayManager
- * @deprecated 101.1.0
- */
- private function getArrayManager(): ArrayManager
- {
- if (null === $this->arrayManager) {
- $this->arrayManager = \Magento\Framework\App\ObjectManager::getInstance()->get(
- ArrayManager::class
- );
- }
-
- return $this->arrayManager;
- }
-
- /**
- * Get FileInfo instance
- *
- * @return FileInfo
- *
- * @deprecated 101.1.0
- */
- private function getFileInfo(): FileInfo
- {
- if ($this->fileInfo === null) {
- $this->fileInfo = ObjectManager::getInstance()->get(FileInfo::class);
- }
- return $this->fileInfo;
- }
}
diff --git a/app/code/Magento/Catalog/Model/Config.php b/app/code/Magento/Catalog/Model/Config.php
index 5dce940308a4f..c4ff12bbf0f94 100644
--- a/app/code/Magento/Catalog/Model/Config.php
+++ b/app/code/Magento/Catalog/Model/Config.php
@@ -9,6 +9,8 @@
use Magento\Framework\Serialize\SerializerInterface;
/**
+ * Catalog config model.
+ *
* @SuppressWarnings(PHPMD.LongVariable)
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -133,6 +135,7 @@ class Config extends \Magento\Eav\Model\Config
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param \Magento\Eav\Model\Config $eavConfig
* @param SerializerInterface $serializer
+ * @param array $attributesForPreload
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -149,7 +152,8 @@ public function __construct(
\Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $setCollectionFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Eav\Model\Config $eavConfig,
- SerializerInterface $serializer = null
+ SerializerInterface $serializer = null,
+ $attributesForPreload = []
) {
$this->_scopeConfig = $scopeConfig;
$this->_configFactory = $configFactory;
@@ -165,7 +169,9 @@ public function __construct(
$entityTypeCollectionFactory,
$cacheState,
$universalFactory,
- $serializer
+ $serializer,
+ $scopeConfig,
+ $attributesForPreload
);
}
diff --git a/app/code/Magento/Catalog/Model/Design.php b/app/code/Magento/Catalog/Model/Design.php
index 853bbeac8eb38..fed18a5a60913 100644
--- a/app/code/Magento/Catalog/Model/Design.php
+++ b/app/code/Magento/Catalog/Model/Design.php
@@ -5,6 +5,9 @@
*/
namespace Magento\Catalog\Model;
+use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager as CategoryLayoutManager;
+use Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager as ProductLayoutManager;
+use Magento\Framework\App\ObjectManager;
use \Magento\Framework\TranslateInterface;
/**
@@ -14,6 +17,7 @@
*
* @author Magento Core Team
* @since 100.0.2
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Design extends \Magento\Framework\Model\AbstractModel
{
@@ -38,6 +42,16 @@ class Design extends \Magento\Framework\Model\AbstractModel
*/
private $translator;
+ /**
+ * @var CategoryLayoutManager
+ */
+ private $categoryLayoutUpdates;
+
+ /**
+ * @var ProductLayoutManager
+ */
+ private $productLayoutUpdates;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -47,6 +61,9 @@ class Design extends \Magento\Framework\Model\AbstractModel
* @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection
* @param array $data
* @param TranslateInterface|null $translator
+ * @param CategoryLayoutManager|null $categoryLayoutManager
+ * @param ProductLayoutManager|null $productLayoutManager
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Framework\Model\Context $context,
@@ -56,12 +73,17 @@ public function __construct(
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
array $data = [],
- TranslateInterface $translator = null
+ TranslateInterface $translator = null,
+ ?CategoryLayoutManager $categoryLayoutManager = null,
+ ?ProductLayoutManager $productLayoutManager = null
) {
$this->_localeDate = $localeDate;
$this->_design = $design;
- $this->translator = $translator ?:
- \Magento\Framework\App\ObjectManager::getInstance()->get(TranslateInterface::class);
+ $this->translator = $translator ?? ObjectManager::getInstance()->get(TranslateInterface::class);
+ $this->categoryLayoutUpdates = $categoryLayoutManager
+ ?? ObjectManager::getInstance()->get(CategoryLayoutManager::class);
+ $this->productLayoutUpdates = $productLayoutManager
+ ?? ObjectManager::getInstance()->get(ProductLayoutManager::class);
parent::__construct($context, $registry, $resource, $resourceCollection, $data);
}
@@ -81,12 +103,12 @@ public function applyCustomDesign($design)
/**
* Get custom layout settings
*
- * @param \Magento\Catalog\Model\Category|\Magento\Catalog\Model\Product $object
+ * @param Category|Product $object
* @return \Magento\Framework\DataObject
*/
public function getDesignSettings($object)
{
- if ($object instanceof \Magento\Catalog\Model\Product) {
+ if ($object instanceof Product) {
$currentCategory = $object->getCategory();
} else {
$currentCategory = $object;
@@ -97,7 +119,7 @@ public function getDesignSettings($object)
$category = $currentCategory->getParentDesignCategory($currentCategory);
}
- if ($object instanceof \Magento\Catalog\Model\Product) {
+ if ($object instanceof Product) {
if ($category && $category->getCustomApplyToProducts()) {
return $this->_mergeSettings($this->_extractSettings($category), $this->_extractSettings($object));
} else {
@@ -111,7 +133,7 @@ public function getDesignSettings($object)
/**
* Extract custom layout settings from category or product object
*
- * @param \Magento\Catalog\Model\Category|\Magento\Catalog\Model\Product $object
+ * @param Category|Product $object
* @return \Magento\Framework\DataObject
*/
protected function _extractSettings($object)
@@ -140,6 +162,11 @@ protected function _extractSettings($object)
)->setLayoutUpdates(
(array)$object->getCustomLayoutUpdate()
);
+ if ($object instanceof Category) {
+ $this->categoryLayoutUpdates->extractCustomSettings($object, $settings);
+ } elseif ($object instanceof Product) {
+ $this->productLayoutUpdates->extractCustomSettings($object, $settings);
+ }
}
return $settings;
}
diff --git a/app/code/Magento/Catalog/Model/Entity/Product/Attribute/Design/Options/Container.php b/app/code/Magento/Catalog/Model/Entity/Product/Attribute/Design/Options/Container.php
index 22cb3c3264df5..d9893be3125fe 100644
--- a/app/code/Magento/Catalog/Model/Entity/Product/Attribute/Design/Options/Container.php
+++ b/app/code/Magento/Catalog/Model/Entity/Product/Attribute/Design/Options/Container.php
@@ -21,7 +21,7 @@ class Container extends \Magento\Eav\Model\Entity\Attribute\Source\Config
public function getOptionText($value)
{
$options = $this->getAllOptions();
- if (sizeof($options) > 0) {
+ if (count($options) > 0) {
foreach ($options as $option) {
if (isset($option['value']) && $option['value'] == $value) {
return __($option['label']);
diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php
index 825823276261f..0c3e008fa8bb5 100644
--- a/app/code/Magento/Catalog/Model/ImageUploader.php
+++ b/app/code/Magento/Catalog/Model/ImageUploader.php
@@ -191,12 +191,12 @@ public function getFilePath($path, $imageName)
* Checking file for moving and move it
*
* @param string $imageName
- *
+ * @param bool $returnRelativePath
* @return string
*
* @throws \Magento\Framework\Exception\LocalizedException
*/
- public function moveFileFromTmp($imageName)
+ public function moveFileFromTmp($imageName, $returnRelativePath = false)
{
$baseTmpPath = $this->getBaseTmpPath();
$basePath = $this->getBasePath();
@@ -226,7 +226,7 @@ public function moveFileFromTmp($imageName)
);
}
- return $imageName;
+ return $returnRelativePath ? $baseImagePath : $imageName;
}
/**
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product.php
index af1cda41d8c46..c18404bda1fc8 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product.php
@@ -137,10 +137,11 @@ protected function executeAction($ids)
/** @var Product\Action\Rows $action */
$action = $this->rowsActionFactory->create();
- if ($indexer->isWorking()) {
+ if ($indexer->isScheduled()) {
$action->execute($ids, true);
+ } else {
+ $action->execute($ids);
}
- $action->execute($ids);
return $this;
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php
index 3bd4910767587..5d81c1405efe0 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php
@@ -5,6 +5,25 @@
*/
namespace Magento\Catalog\Model\Indexer\Category\Product\Action;
+use Magento\Framework\DB\Adapter\AdapterInterface;
+use Magento\Framework\Indexer\CacheContext;
+use Magento\Framework\Event\ManagerInterface as EventManagerInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\App\ResourceConnection;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Framework\DB\Query\Generator as QueryGenerator;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Catalog\Model\Config;
+use Magento\Catalog\Model\Category;
+use Magento\Framework\Indexer\IndexerRegistry;
+use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer;
+
+/**
+ * Reindex multiple rows action.
+ *
+ * @package Magento\Catalog\Model\Indexer\Category\Product\Action
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction
{
/**
@@ -14,25 +33,121 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio
*/
protected $limitationByCategories;
+ /**
+ * @var CacheContext
+ */
+ private $cacheContext;
+
+ /**
+ * @var EventManagerInterface|null
+ */
+ private $eventManager;
+
+ /**
+ * @var IndexerRegistry
+ */
+ private $indexerRegistry;
+
+ /**
+ * @param ResourceConnection $resource
+ * @param StoreManagerInterface $storeManager
+ * @param Config $config
+ * @param QueryGenerator|null $queryGenerator
+ * @param MetadataPool|null $metadataPool
+ * @param CacheContext|null $cacheContext
+ * @param EventManagerInterface|null $eventManager
+ * @param IndexerRegistry|null $indexerRegistry
+ */
+ public function __construct(
+ ResourceConnection $resource,
+ StoreManagerInterface $storeManager,
+ Config $config,
+ QueryGenerator $queryGenerator = null,
+ MetadataPool $metadataPool = null,
+ CacheContext $cacheContext = null,
+ EventManagerInterface $eventManager = null,
+ IndexerRegistry $indexerRegistry = null
+ ) {
+ parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool);
+ $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class);
+ $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class);
+ $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class);
+ }
+
/**
* Refresh entities index
*
* @param int[] $entityIds
* @param bool $useTempTable
* @return $this
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function execute(array $entityIds = [], $useTempTable = false)
{
- $this->limitationByCategories = $entityIds;
+ foreach ($entityIds as $entityId) {
+ $this->limitationByCategories[] = (int)$entityId;
+ $path = $this->getPathFromCategoryId($entityId);
+ if (!empty($path)) {
+ $pathIds = explode('/', $path);
+ foreach ($pathIds as $pathId) {
+ $this->limitationByCategories[] = (int)$pathId;
+ }
+ }
+ }
+ $this->limitationByCategories = array_unique($this->limitationByCategories);
$this->useTempTable = $useTempTable;
+ $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID);
+ $workingState = $indexer->isWorking();
- $this->removeEntries();
+ if ($useTempTable && !$workingState && $indexer->isScheduled()) {
+ foreach ($this->storeManager->getStores() as $store) {
+ $this->connection->truncateTable($this->getIndexTable($store->getId()));
+ }
+ } else {
+ $this->removeEntries();
+ }
$this->reindex();
+ if ($useTempTable && !$workingState && $indexer->isScheduled()) {
+ foreach ($this->storeManager->getStores() as $store) {
+ $removalCategoryIds = array_diff($this->limitationByCategories, [$this->getRootCategoryId($store)]);
+ $this->connection->delete(
+ $this->tableMaintainer->getMainTable($store->getId()),
+ ['category_id IN (?)' => $removalCategoryIds]
+ );
+ $select = $this->connection->select()
+ ->from($this->tableMaintainer->getMainReplicaTable($store->getId()));
+ $this->connection->query(
+ $this->connection->insertFromSelect(
+ $select,
+ $this->tableMaintainer->getMainTable($store->getId()),
+ [],
+ AdapterInterface::INSERT_ON_DUPLICATE
+ )
+ );
+ }
+ }
+
+ $this->registerCategories($entityIds);
+ $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
+
return $this;
}
+ /**
+ * Register categories assigned to products
+ *
+ * @param array $categoryIds
+ * @return void
+ */
+ private function registerCategories(array $categoryIds)
+ {
+ if ($categoryIds) {
+ $this->cacheContext->registerEntities(Category::CACHE_TAG, $categoryIds);
+ }
+ }
+
/**
* Return array of all category root IDs + tree root ID
*
diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php
index 15ba6c8f3758b..ec3d0d57330ec 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php
@@ -15,6 +15,9 @@
use Magento\Framework\Event\ManagerInterface as EventManagerInterface;
use Magento\Framework\Indexer\CacheContext;
use Magento\Store\Model\StoreManagerInterface;
+use Magento\Framework\DB\Adapter\AdapterInterface;
+use Magento\Framework\Indexer\IndexerRegistry;
+use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer;
/**
* Category rows indexer.
@@ -40,6 +43,11 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio
*/
private $eventManager;
+ /**
+ * @var IndexerRegistry
+ */
+ private $indexerRegistry;
+
/**
* @param ResourceConnection $resource
* @param StoreManagerInterface $storeManager
@@ -48,6 +56,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio
* @param MetadataPool|null $metadataPool
* @param CacheContext|null $cacheContext
* @param EventManagerInterface|null $eventManager
+ * @param IndexerRegistry|null $indexerRegistry
*/
public function __construct(
ResourceConnection $resource,
@@ -56,11 +65,13 @@ public function __construct(
QueryGenerator $queryGenerator = null,
MetadataPool $metadataPool = null,
CacheContext $cacheContext = null,
- EventManagerInterface $eventManager = null
+ EventManagerInterface $eventManager = null,
+ IndexerRegistry $indexerRegistry = null
) {
parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool);
$this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class);
$this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class);
+ $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class);
}
/**
@@ -78,12 +89,37 @@ public function execute(array $entityIds = [], $useTempTable = false)
$this->limitationByProducts = $idsToBeReIndexed;
$this->useTempTable = $useTempTable;
+ $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID);
+ $workingState = $indexer->isWorking();
$affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed);
- $this->removeEntries();
-
+ if ($useTempTable && !$workingState && $indexer->isScheduled()) {
+ foreach ($this->storeManager->getStores() as $store) {
+ $this->connection->truncateTable($this->getIndexTable($store->getId()));
+ }
+ } else {
+ $this->removeEntries();
+ }
$this->reindex();
+ if ($useTempTable && !$workingState && $indexer->isScheduled()) {
+ foreach ($this->storeManager->getStores() as $store) {
+ $this->connection->delete(
+ $this->tableMaintainer->getMainTable($store->getId()),
+ ['product_id IN (?)' => $this->limitationByProducts]
+ );
+ $select = $this->connection->select()
+ ->from($this->tableMaintainer->getMainReplicaTable($store->getId()));
+ $this->connection->query(
+ $this->connection->insertFromSelect(
+ $select,
+ $this->tableMaintainer->getMainTable($store->getId()),
+ [],
+ AdapterInterface::INSERT_ON_DUPLICATE
+ )
+ );
+ }
+ }
$affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed));
diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php
index d1aee8c4c5ba6..229844fbe84b5 100644
--- a/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php
+++ b/app/code/Magento/Catalog/Model/Layer/Filter/DataProvider/Price.php
@@ -10,6 +10,9 @@
use Magento\Framework\Registry;
use Magento\Store\Model\ScopeInterface;
+/**
+ * Data provider for price filter in layered navigation
+ */
class Price
{
/**
@@ -103,6 +106,8 @@ public function __construct(
}
/**
+ * Getter for interval
+ *
* @return array
*/
public function getInterval()
@@ -111,6 +116,8 @@ public function getInterval()
}
/**
+ * Setter for interval
+ *
* @param array $interval
* @return void
*/
@@ -120,6 +127,10 @@ public function setInterval($interval)
}
/**
+ * Retrieves price layered navigation modes
+ *
+ * @see RANGE_CALCULATION_AUTO
+ *
* @return mixed
*/
public function getRangeCalculationValue()
@@ -131,6 +142,8 @@ public function getRangeCalculationValue()
}
/**
+ * Retrieves range step
+ *
* @return mixed
*/
public function getRangeStepValue()
@@ -142,6 +155,8 @@ public function getRangeStepValue()
}
/**
+ * Retrieves one price interval
+ *
* @return mixed
*/
public function getOnePriceIntervalValue()
@@ -179,6 +194,8 @@ public function getRangeMaxIntervalsValue()
}
/**
+ * Retrieves Catalog Layer object
+ *
* @return Layer
*/
public function getLayer()
@@ -276,6 +293,8 @@ public function getMaxPrice()
}
/**
+ * Retrieve list of prior filters
+ *
* @param string $filterParams
* @return array
*/
@@ -310,7 +329,7 @@ public function validateFilter($filter)
return false;
}
foreach ($filter as $v) {
- if ($v !== '' && $v !== '0' && (double)$v <= 0 || is_infinite((double)$v)) {
+ if ($v !== '' && $v !== '0' && (!is_numeric($v) || (double)$v <= 0 || is_infinite((double)$v))) {
return false;
}
}
@@ -339,6 +358,8 @@ public function getResetValue()
}
/**
+ * Getter for prior intervals
+ *
* @return array
*/
public function getPriorIntervals()
@@ -347,6 +368,8 @@ public function getPriorIntervals()
}
/**
+ * Setter for prior intervals
+ *
* @param array $priorInterval
* @return void
*/
@@ -356,6 +379,8 @@ public function setPriorIntervals($priorInterval)
}
/**
+ * Get Resource model for price filter
+ *
* @return \Magento\Catalog\Model\ResourceModel\Layer\Filter\Price
*/
public function getResource()
@@ -364,6 +389,8 @@ public function getResource()
}
/**
+ * Retrieves additional request data
+ *
* @return string
*/
public function getAdditionalRequestData()
diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php
index 8092ff3eb9d4a..7015fa0295cfb 100644
--- a/app/code/Magento/Catalog/Model/Product.php
+++ b/app/code/Magento/Catalog/Model/Product.php
@@ -5,7 +5,6 @@
*/
namespace Magento\Catalog\Model;
-use Magento\Authorization\Model\UserContextInterface;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
@@ -15,7 +14,6 @@
use Magento\Framework\Api\AttributeValueFactory;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\ObjectManager;
-use Magento\Framework\AuthorizationInterface;
use Magento\Framework\DataObject\IdentityInterface;
use Magento\Framework\Pricing\SaleableInterface;
@@ -355,16 +353,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
*/
private $filterCustomAttribute;
- /**
- * @var UserContextInterface
- */
- private $userContext;
-
- /**
- * @var AuthorizationInterface
- */
- private $authorization;
-
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -837,11 +825,12 @@ public function getStoreIds()
}
foreach ($websiteIds as $websiteId) {
$websiteStores = $this->_storeManager->getWebsite($websiteId)->getStoreIds();
- foreach ($websiteStores as $websiteStore) {
- $storeIds []= $websiteStore;
- }
+ $storeIds[] = $websiteStores;
}
}
+ if ($storeIds) {
+ $storeIds = array_merge(...$storeIds);
+ }
$this->setStoreIds($storeIds);
}
return $this->getData('store_ids');
@@ -874,34 +863,6 @@ public function getAttributes($groupId = null, $skipSuper = false)
return $attributes;
}
- /**
- * Get user context.
- *
- * @return UserContextInterface
- */
- private function getUserContext(): UserContextInterface
- {
- if (!$this->userContext) {
- $this->userContext = ObjectManager::getInstance()->get(UserContextInterface::class);
- }
-
- return $this->userContext;
- }
-
- /**
- * Get authorization service.
- *
- * @return AuthorizationInterface
- */
- private function getAuthorization(): AuthorizationInterface
- {
- if (!$this->authorization) {
- $this->authorization = ObjectManager::getInstance()->get(AuthorizationInterface::class);
- }
-
- return $this->authorization;
- }
-
/**
* Check product options and type options and save them, too
*
@@ -919,22 +880,6 @@ public function beforeSave()
$this->getTypeInstance()->beforeSave($this);
- //Validate changing of design.
- $userType = $this->getUserContext()->getUserType();
- if ((
- $userType === UserContextInterface::USER_TYPE_ADMIN
- || $userType === UserContextInterface::USER_TYPE_INTEGRATION
- )
- && !$this->getAuthorization()->isAllowed('Magento_Catalog::edit_product_design')
- ) {
- $this->setData('custom_design', $this->getOrigData('custom_design'));
- $this->setData('page_layout', $this->getOrigData('page_layout'));
- $this->setData('options_container', $this->getOrigData('options_container'));
- $this->setData('custom_layout_update', $this->getOrigData('custom_layout_update'));
- $this->setData('custom_design_from', $this->getOrigData('custom_design_from'));
- $this->setData('custom_design_to', $this->getOrigData('custom_design_to'));
- }
-
$hasOptions = false;
$hasRequiredOptions = false;
@@ -1326,12 +1271,11 @@ public function getSpecialToDate()
public function getRelatedProducts()
{
if (!$this->hasRelatedProducts()) {
- $products = [];
- $collection = $this->getRelatedProductCollection();
- foreach ($collection as $product) {
- $products[] = $product;
+ //Loading all linked products.
+ $this->getProductLinks();
+ if (!$this->hasRelatedProducts()) {
+ $this->setRelatedProducts([]);
}
- $this->setRelatedProducts($products);
}
return $this->getData('related_products');
}
@@ -1388,12 +1332,13 @@ public function getRelatedLinkCollection()
public function getUpSellProducts()
{
if (!$this->hasUpSellProducts()) {
- $products = [];
- foreach ($this->getUpSellProductCollection() as $product) {
- $products[] = $product;
+ //Loading all linked products.
+ $this->getProductLinks();
+ if (!$this->hasUpSellProducts()) {
+ $this->setUpSellProducts([]);
}
- $this->setUpSellProducts($products);
}
+
return $this->getData('up_sell_products');
}
@@ -1449,12 +1394,13 @@ public function getUpSellLinkCollection()
public function getCrossSellProducts()
{
if (!$this->hasCrossSellProducts()) {
- $products = [];
- foreach ($this->getCrossSellProductCollection() as $product) {
- $products[] = $product;
+ //Loading all linked products.
+ $this->getProductLinks();
+ if (!$this->hasCrossSellProducts()) {
+ $this->setCrossSellProducts([]);
}
- $this->setCrossSellProducts($products);
}
+
return $this->getData('cross_sell_products');
}
@@ -1510,7 +1456,11 @@ public function getCrossSellLinkCollection()
public function getProductLinks()
{
if ($this->_links === null) {
- $this->_links = $this->getLinkRepository()->getList($this);
+ if ($this->getSku() && $this->getId()) {
+ $this->_links = $this->getLinkRepository()->getList($this);
+ } else {
+ $this->_links = [];
+ }
}
return $this->_links;
}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php
index e26717e47274c..68aeabfc70d34 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php
@@ -97,17 +97,16 @@ protected function _getWebsiteCurrencyRates()
);
foreach ($this->_storeManager->getWebsites() as $website) {
/* @var $website \Magento\Store\Model\Website */
- if ($website->getBaseCurrencyCode() != $baseCurrency) {
+ $websiteBaseCurrency = $website->getBaseCurrencyCode();
+ if ($websiteBaseCurrency !== $baseCurrency) {
$rate = $this->_currencyFactory->create()->load(
$baseCurrency
- )->getRate(
- $website->getBaseCurrencyCode()
- );
+ )->getRate($websiteBaseCurrency);
if (!$rate) {
$rate = 1;
}
$this->_rates[$website->getId()] = [
- 'code' => $website->getBaseCurrencyCode(),
+ 'code' => $websiteBaseCurrency,
'rate' => $rate,
];
} else {
@@ -187,6 +186,7 @@ public function validate($object)
}
$compare = implode(
'-',
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
array_merge(
[$priceRow['website_id'], $priceRow['cust_group']],
$this->_getAdditionalUniqueFields($priceRow)
@@ -210,6 +210,7 @@ public function validate($object)
if ($price['website_id'] == 0) {
$compare = implode(
'-',
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
array_merge(
[$price['website_id'], $price['cust_group']],
$this->_getAdditionalUniqueFields($price)
@@ -234,6 +235,7 @@ public function validate($object)
$globalCompare = implode(
'-',
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
array_merge([0, $priceRow['cust_group']], $this->_getAdditionalUniqueFields($priceRow))
);
$websiteCurrency = $rates[$priceRow['website_id']]['code'];
@@ -279,6 +281,7 @@ public function preparePriceData(array $priceData, $productTypeId, $websiteId)
if (!array_filter($v)) {
continue;
}
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$key = implode('-', array_merge([$v['cust_group']], $this->_getAdditionalUniqueFields($v)));
if ($v['website_id'] == $websiteId) {
$data[$key] = $v;
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php
new file mode 100644
index 0000000000000..fa5a218824eea
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/LayoutUpdate.php
@@ -0,0 +1,44 @@
+manager = $manager;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param AbstractModel|Product $forModel
+ */
+ protected function listAvailableValues(AbstractModel $forModel): array
+ {
+ return $this->manager->fetchAvailableFiles($forModel);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php
index f1943bc108878..0daa1dfb5c8eb 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/TierPrice/UpdateHandler.php
@@ -96,12 +96,7 @@ public function execute($entity, $arguments = [])
$productId = (int)$entity->getData($identifierField);
// prepare original data to compare
- $origPrices = [];
- $originalId = $entity->getOrigData($identifierField);
- if (empty($originalId) || $entity->getData($identifierField) == $originalId) {
- $origPrices = $entity->getOrigData($attribute->getName());
- }
-
+ $origPrices = $entity->getOrigData($attribute->getName());
$old = $this->prepareOldTierPriceToCompare($origPrices);
// prepare data for save
$new = $this->prepareNewDataForSave($priceRows, $isGlobal);
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php b/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php
new file mode 100644
index 0000000000000..92ae989500076
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/LayoutUpdateManager.php
@@ -0,0 +1,166 @@
+themeFactory = $themeFactory;
+ $this->design = $design;
+ $this->layoutProcessorFactory = $layoutProcessorFactory;
+ }
+
+ /**
+ * Adopt product's SKU to be used as layout handle.
+ *
+ * @param ProductInterface $product
+ * @return string
+ */
+ private function sanitizeSku(ProductInterface $product): string
+ {
+ return rawurlencode($product->getSku());
+ }
+
+ /**
+ * Get the processor instance.
+ *
+ * @return LayoutProcessor
+ */
+ private function getLayoutProcessor(): LayoutProcessor
+ {
+ if (!$this->layoutProcessor) {
+ $this->layoutProcessor = $this->layoutProcessorFactory->create(
+ [
+ 'theme' => $this->themeFactory->create(
+ $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND)
+ )
+ ]
+ );
+ $this->themeFactory = null;
+ $this->design = null;
+ }
+
+ return $this->layoutProcessor;
+ }
+
+ /**
+ * Fetch list of available files/handles for the product.
+ *
+ * @param ProductInterface $product
+ * @return string[]
+ */
+ public function fetchAvailableFiles(ProductInterface $product): array
+ {
+ if (!$product->getSku()) {
+ return [];
+ }
+
+ $identifier = $this->sanitizeSku($product);
+ $handles = $this->getLayoutProcessor()->getAvailableHandles();
+
+ return array_filter(
+ array_map(
+ function (string $handle) use ($identifier) : ?string {
+ preg_match(
+ '/^catalog\_product\_view\_selectable\_' .preg_quote($identifier) .'\_([a-z0-9]+)/i',
+ $handle,
+ $selectable
+ );
+ if (!empty($selectable[1])) {
+ return $selectable[1];
+ }
+
+ return null;
+ },
+ $handles
+ )
+ );
+ }
+
+ /**
+ * Extract custom layout attribute value.
+ *
+ * @param ProductInterface $product
+ * @return mixed
+ */
+ private function extractAttributeValue(ProductInterface $product)
+ {
+ if ($product instanceof Product && !$product->hasData(ProductInterface::CUSTOM_ATTRIBUTES)) {
+ return $product->getData('custom_layout_update_file');
+ }
+ if ($attr = $product->getCustomAttribute('custom_layout_update_file')) {
+ return $attr->getValue();
+ }
+
+ return null;
+ }
+
+ /**
+ * Extract selected custom layout settings.
+ *
+ * If no update is selected none will apply.
+ *
+ * @param ProductInterface $product
+ * @param DataObject $intoSettings
+ * @return void
+ */
+ public function extractCustomSettings(ProductInterface $product, DataObject $intoSettings): void
+ {
+ if ($product->getSku() && $value = $this->extractAttributeValue($product)) {
+ $handles = $intoSettings->getPageLayoutHandles() ?? [];
+ $handles = array_merge_recursive(
+ $handles,
+ ['selectable' => $this->sanitizeSku($product) . '_' . $value]
+ );
+ $intoSettings->setPageLayoutHandles($handles);
+ }
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php
new file mode 100644
index 0000000000000..0ddb528e768cc
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/LayoutUpdate.php
@@ -0,0 +1,40 @@
+manager = $manager;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function listAvailableOptions(CustomAttributesDataInterface $entity): array
+ {
+ return $this->manager->fetchAvailableFiles($entity);
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Authorization.php b/app/code/Magento/Catalog/Model/Product/Authorization.php
new file mode 100644
index 0000000000000..b8aa8f70ba70f
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Authorization.php
@@ -0,0 +1,170 @@
+authorization = $authorization;
+ $this->productFactory = $factory;
+ }
+
+ /**
+ * Extract attribute value from the model.
+ *
+ * @param ProductModel $product
+ * @param AttributeInterface $attr
+ * @return mixed
+ * @throws \RuntimeException When no new value is present.
+ */
+ private function extractAttributeValue(ProductModel $product, AttributeInterface $attr)
+ {
+ if ($product->hasData($attr->getAttributeCode())) {
+ $newValue = $product->getData($attr->getAttributeCode());
+ } elseif ($product->hasData(ProductModel::CUSTOM_ATTRIBUTES)
+ && $attrValue = $product->getCustomAttribute($attr->getAttributeCode())
+ ) {
+ $newValue = $attrValue->getValue();
+ } else {
+ throw new \RuntimeException('No new value is present');
+ }
+
+ if (empty($newValue)
+ || ($attr->getBackend() instanceof LayoutUpdate
+ && ($newValue === LayoutUpdate::VALUE_USE_UPDATE_XML || $newValue === LayoutUpdate::VALUE_NO_UPDATE)
+ )
+ ) {
+ $newValue = null;
+ }
+
+ return $newValue;
+ }
+
+ /**
+ * Prepare old values to compare to.
+ *
+ * @param AttributeInterface $attribute
+ * @param array|null $oldProduct
+ * @return array
+ */
+ private function fetchOldValues(AttributeInterface $attribute, ?array $oldProduct): array
+ {
+ $attrCode = $attribute->getAttributeCode();
+ if ($oldProduct) {
+ //New value may only be the saved value
+ $oldValues = [!empty($oldProduct[$attrCode]) ? $oldProduct[$attrCode] : null];
+ if (empty($oldValues[0])) {
+ $oldValues[0] = null;
+ }
+ } else {
+ //New value can be empty or default
+ $oldValues[] = $attribute->getDefaultValue();
+ }
+
+ return $oldValues;
+ }
+
+ /**
+ * Check whether the product has changed.
+ *
+ * @param ProductModel $product
+ * @param array|null $oldProduct
+ * @return bool
+ */
+ private function hasProductChanged(ProductModel $product, ?array $oldProduct = null): bool
+ {
+ $designAttributes = [
+ 'custom_design',
+ 'page_layout',
+ 'options_container',
+ 'custom_layout_update',
+ 'custom_design_from',
+ 'custom_design_to',
+ 'custom_layout_update_file'
+ ];
+ $attributes = $product->getAttributes();
+
+ foreach ($designAttributes as $designAttribute) {
+ if (!array_key_exists($designAttribute, $attributes)) {
+ continue;
+ }
+ $attribute = $attributes[$designAttribute];
+ $oldValues = $this->fetchOldValues($attribute, $oldProduct);
+ try {
+ $newValue = $this->extractAttributeValue($product, $attribute);
+ } catch (\RuntimeException $exception) {
+ //No new value
+ continue;
+ }
+ if (!in_array($newValue, $oldValues, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Authorize saving of a product.
+ *
+ * @throws AuthorizationException
+ * @throws NoSuchEntityException When product with invalid ID given.
+ * @param ProductInterface|ProductModel $product
+ * @return void
+ */
+ public function authorizeSavingOf(ProductInterface $product): void
+ {
+ if (!$this->authorization->isAllowed('Magento_Catalog::edit_product_design')) {
+ $oldData = null;
+ if ($product->getId()) {
+ if ($product->getOrigData()) {
+ $oldData = $product->getOrigData();
+ } else {
+ /** @var ProductModel $savedProduct */
+ $savedProduct = $this->productFactory->create();
+ $savedProduct->load($product->getId());
+ if (!$savedProduct->getSku()) {
+ throw NoSuchEntityException::singleField('id', $product->getId());
+ }
+ $oldData = $product->getOrigData();
+ }
+ }
+ if ($this->hasProductChanged($product, $oldData)) {
+ throw new AuthorizationException(__('Not allowed to edit the product\'s design attributes'));
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Copier.php b/app/code/Magento/Catalog/Model/Product/Copier.php
index 44ebdf0f1f283..a7f7bad1a5167 100644
--- a/app/code/Magento/Catalog/Model/Product/Copier.php
+++ b/app/code/Magento/Catalog/Model/Product/Copier.php
@@ -6,7 +6,9 @@
namespace Magento\Catalog\Model\Product;
use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Catalog\Model\Attribute\ScopeOverriddenValue;
use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\ProductFactory;
/**
* Catalog product copier.
@@ -28,7 +30,7 @@ class Copier
protected $copyConstructor;
/**
- * @var \Magento\Catalog\Model\ProductFactory
+ * @var ProductFactory
*/
protected $productFactory;
@@ -36,17 +38,24 @@ class Copier
* @var \Magento\Framework\EntityManager\MetadataPool
*/
protected $metadataPool;
+ /**
+ * @var ScopeOverriddenValue
+ */
+ private $scopeOverriddenValue;
/**
* @param CopyConstructorInterface $copyConstructor
- * @param \Magento\Catalog\Model\ProductFactory $productFactory
+ * @param ProductFactory $productFactory
+ * @param ScopeOverriddenValue $scopeOverriddenValue
*/
public function __construct(
CopyConstructorInterface $copyConstructor,
- \Magento\Catalog\Model\ProductFactory $productFactory
+ ProductFactory $productFactory,
+ ScopeOverriddenValue $scopeOverriddenValue
) {
$this->productFactory = $productFactory;
$this->copyConstructor = $copyConstructor;
+ $this->scopeOverriddenValue = $scopeOverriddenValue;
}
/**
@@ -121,19 +130,20 @@ private function setStoresUrl(Product $product, Product $duplicate) : void
$storeIds = $duplicate->getStoreIds();
$productId = $product->getId();
$productResource = $product->getResource();
- $defaultUrlKey = $productResource->getAttributeRawValue(
- $productId,
- 'url_key',
- \Magento\Store\Model\Store::DEFAULT_STORE_ID
- );
$duplicate->setData('save_rewrites_history', false);
foreach ($storeIds as $storeId) {
+ $useDefault = !$this->scopeOverriddenValue->containsValue(
+ ProductInterface::class,
+ $product,
+ 'url_key',
+ $storeId
+ );
+ if ($useDefault) {
+ continue;
+ }
$isDuplicateSaved = false;
$duplicate->setStoreId($storeId);
$urlKey = $productResource->getAttributeRawValue($productId, 'url_key', $storeId);
- if ($urlKey === $defaultUrlKey) {
- continue;
- }
do {
$urlKey = $this->modifyUrl($urlKey);
$duplicate->setUrlKey($urlKey);
diff --git a/app/code/Magento/Catalog/Model/Product/Filter/DateTime.php b/app/code/Magento/Catalog/Model/Product/Filter/DateTime.php
new file mode 100644
index 0000000000000..93b7d55458a9f
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Filter/DateTime.php
@@ -0,0 +1,67 @@
+stdlibDateTimeFilter = $stdlibDateTimeFilter;
+ }
+
+ /**
+ * Convert datetime from locale format to internal format;
+ *
+ * Make an additional check for MySql date format which is wrongly parsed by IntlDateFormatter
+ *
+ * @param mixed $value
+ * @return mixed|string
+ * @throws \Exception
+ */
+ public function filter($value)
+ {
+ if (is_string($value)) {
+ $value = $this->createDateFromMySqlFormat($value) ?? $value;
+ }
+ return $this->stdlibDateTimeFilter->filter($value);
+ }
+
+ /**
+ * Parse a string in MySql date format into a new DateTime object
+ *
+ * @param string $value
+ * @return \DateTime|null
+ */
+ private function createDateFromMySqlFormat(string $value): ?\DateTime
+ {
+ $datetime = date_create_from_format(StdlibDateTime::DATETIME_PHP_FORMAT, $value);
+ if ($datetime === false) {
+ $datetime = date_create_from_format(StdlibDateTime::DATE_PHP_FORMAT, $value);
+ if ($datetime !== false) {
+ $datetime->setTime(0, 0, 0, 0);
+ }
+ }
+ return $datetime instanceof \DateTime ? $datetime : null;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php
index b374b754d7de1..225a3a4c44a9b 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php
@@ -162,7 +162,7 @@ public function execute($product, $arguments = [])
if (!empty($image['removed'])) {
$clearImages[] = $image['file'];
- } elseif (empty($image['value_id'])) {
+ } elseif (empty($image['value_id']) || !empty($image['recreate'])) {
$newFile = $this->moveImageFromTmp($image['file']);
$image['new_file'] = $newFile;
$newImages[$image['file']] = $image;
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php
index 9e5cf084c25a1..a9afb7cec45e2 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php
@@ -71,10 +71,12 @@ public function create($sku, ProductAttributeMediaGalleryEntryInterface $entry)
$product->setMediaGalleryEntries($existingMediaGalleryEntries);
try {
$product = $this->productRepository->save($product);
- } catch (InputException $inputException) {
- throw $inputException;
} catch (\Exception $e) {
- throw new StateException(__("The product can't be saved."));
+ if ($e instanceof InputException) {
+ throw $e;
+ } else {
+ throw new StateException(__("The product can't be saved."));
+ }
}
foreach ($product->getMediaGalleryEntries() as $entry) {
@@ -98,19 +100,13 @@ public function update($sku, ProductAttributeMediaGalleryEntryInterface $entry)
);
}
$found = false;
+ $entryTypes = (array)$entry->getTypes();
foreach ($existingMediaGalleryEntries as $key => $existingEntry) {
- $entryTypes = (array)$entry->getTypes();
- $existingEntryTypes = (array)$existingMediaGalleryEntries[$key]->getTypes();
- $existingMediaGalleryEntries[$key]->setTypes(array_diff($existingEntryTypes, $entryTypes));
+ $existingEntryTypes = (array)$existingEntry->getTypes();
+ $existingEntry->setTypes(array_diff($existingEntryTypes, $entryTypes));
if ($existingEntry->getId() == $entry->getId()) {
$found = true;
-
- $file = $entry->getContent();
-
- if ($file && $file->getBase64EncodedData() || $entry->getFile()) {
- $entry->setId(null);
- }
$existingMediaGalleryEntries[$key] = $entry;
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
index 189135776b68b..049846ef36490 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php
@@ -5,6 +5,7 @@
*/
namespace Magento\Catalog\Model\Product\Gallery;
+use Magento\Catalog\Model\ResourceModel\Product\Gallery;
use Magento\Framework\EntityManager\Operation\ExtensionInterface;
/**
@@ -75,6 +76,16 @@ protected function processNewImage($product, array &$image)
$image['value_id'],
$product->getData($this->metadata->getLinkField())
);
+ } elseif (!empty($image['recreate'])) {
+ $data['value_id'] = $image['value_id'];
+ $data['value'] = $image['file'];
+ $data['attribute_id'] = $this->getAttribute()->getAttributeId();
+
+ if (!empty($image['media_type'])) {
+ $data['media_type'] = $image['media_type'];
+ }
+
+ $this->resourceModel->saveDataRow(Gallery::GALLERY_TABLE, $data);
}
return $data;
diff --git a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php
index 4b7a623b15c19..c0f4e83ef3de4 100644
--- a/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php
+++ b/app/code/Magento/Catalog/Model/Product/Image/ParamsBuilder.php
@@ -130,10 +130,12 @@ private function getWatermark(string $type, int $scopeId = null): array
);
if ($file) {
- $size = $this->scopeConfig->getValue(
- "design/watermark/{$type}_size",
- ScopeInterface::SCOPE_STORE,
- $scopeId
+ $size = explode(
+ 'x',
+ $this->scopeConfig->getValue(
+ "design/watermark/{$type}_size",
+ ScopeInterface::SCOPE_STORE
+ )
);
$opacity = $this->scopeConfig->getValue(
"design/watermark/{$type}_imageOpacity",
@@ -145,8 +147,8 @@ private function getWatermark(string $type, int $scopeId = null): array
ScopeInterface::SCOPE_STORE,
$scopeId
);
- $width = !empty($size['width']) ? $size['width'] : null;
- $height = !empty($size['height']) ? $size['height'] : null;
+ $width = !empty($size['0']) ? $size['0'] : null;
+ $height = !empty($size['1']) ? $size['1'] : null;
return [
'watermark_file' => $file,
diff --git a/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php
index a7468bcb1e77f..4a8e6431d6ce8 100644
--- a/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php
+++ b/app/code/Magento/Catalog/Model/Product/Link/SaveHandler.php
@@ -4,14 +4,17 @@
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Model\Product\Link;
use Magento\Catalog\Api\ProductLinkRepositoryInterface;
use Magento\Catalog\Model\ResourceModel\Product\Link;
use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Catalog\Api\Data\ProductLinkInterface;
/**
- * Class SaveProductLinks
+ * Save product links.
*/
class SaveHandler
{
@@ -47,8 +50,10 @@ public function __construct(
}
/**
- * @param string $entityType
- * @param object $entity
+ * Save product links for the product.
+ *
+ * @param string $entityType Product type.
+ * @param \Magento\Catalog\Api\Data\ProductInterface $entity
* @return \Magento\Catalog\Api\Data\ProductInterface
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -56,13 +61,13 @@ public function execute($entityType, $entity)
{
$link = $entity->getData($this->metadataPool->getMetadata($entityType)->getLinkField());
if ($this->linkResource->hasProductLinks($link)) {
- /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */
foreach ($this->productLinkRepository->getList($entity) as $link) {
$this->productLinkRepository->delete($link);
}
}
// Build links per type
+ /** @var ProductLinkInterface[][] $linksByType */
$linksByType = [];
foreach ($entity->getProductLinks() as $link) {
$linksByType[$link->getLinkType()][] = $link;
@@ -71,13 +76,17 @@ public function execute($entityType, $entity)
// Set array position as a fallback position if necessary
foreach ($linksByType as $linkType => $links) {
if (!$this->hasPosition($links)) {
- array_walk($linksByType[$linkType], function ($productLink, $position) {
- $productLink->setPosition(++$position);
- });
+ array_walk(
+ $linksByType[$linkType],
+ function (ProductLinkInterface $productLink, $position) {
+ $productLink->setPosition(++$position);
+ }
+ );
}
}
// Flatten multi-dimensional linksByType in ProductLinks
+ /** @var ProductLinkInterface[] $productLinks */
$productLinks = array_reduce($linksByType, 'array_merge', []);
if (count($productLinks) > 0) {
@@ -90,13 +99,14 @@ public function execute($entityType, $entity)
/**
* Check if at least one link without position
- * @param array $links
+ *
+ * @param ProductLinkInterface[] $links
* @return bool
*/
- private function hasPosition(array $links)
+ private function hasPosition(array $links): bool
{
foreach ($links as $link) {
- if (!array_key_exists('position', $link->getData())) {
+ if ($link->getPosition() === null) {
return false;
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php
index 4f730834412e4..128f420e033c2 100644
--- a/app/code/Magento/Catalog/Model/Product/Option.php
+++ b/app/code/Magento/Catalog/Model/Product/Option.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Catalog\Model\Product;
@@ -11,8 +12,15 @@
use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Option\Type\Date;
+use Magento\Catalog\Model\Product\Option\Type\DefaultType;
+use Magento\Catalog\Model\Product\Option\Type\File;
+use Magento\Catalog\Model\Product\Option\Type\Select;
+use Magento\Catalog\Model\Product\Option\Type\Text;
+use Magento\Catalog\Model\Product\Option\Value;
use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection;
-use Magento\Catalog\Pricing\Price\BasePrice;
+use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Model\AbstractExtensibleModel;
@@ -98,6 +106,16 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter
*/
protected $validatorPool;
+ /**
+ * @var string[]
+ */
+ private $optionGroups;
+
+ /**
+ * @var string[]
+ */
+ private $optionTypesToGroups;
+
/**
* @var MetadataPool
*/
@@ -108,6 +126,11 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter
*/
private $customOptionValuesFactory;
+ /**
+ * @var CalculateCustomOptionCatalogRule
+ */
+ private $calculateCustomOptionCatalogRule;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -121,6 +144,9 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
* @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory
+ * @param array $optionGroups
+ * @param array $optionTypesToGroups
+ * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -135,14 +161,37 @@ public function __construct(
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
array $data = [],
- ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null
+ ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null,
+ array $optionGroups = [],
+ array $optionTypesToGroups = [],
+ CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null
) {
$this->productOptionValue = $productOptionValue;
$this->optionTypeFactory = $optionFactory;
- $this->validatorPool = $validatorPool;
$this->string = $string;
+ $this->validatorPool = $validatorPool;
$this->customOptionValuesFactory = $customOptionValuesFactory ?:
- \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class);
+ ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class);
+ $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ??
+ ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class);
+ $this->optionGroups = $optionGroups ?: [
+ self::OPTION_GROUP_DATE => Date::class,
+ self::OPTION_GROUP_FILE => File::class,
+ self::OPTION_GROUP_SELECT => Select::class,
+ self::OPTION_GROUP_TEXT => Text::class,
+ ];
+ $this->optionTypesToGroups = $optionTypesToGroups ?: [
+ self::OPTION_TYPE_FIELD => self::OPTION_GROUP_TEXT,
+ self::OPTION_TYPE_AREA => self::OPTION_GROUP_TEXT,
+ self::OPTION_TYPE_FILE => self::OPTION_GROUP_FILE,
+ self::OPTION_TYPE_DROP_DOWN => self::OPTION_GROUP_SELECT,
+ self::OPTION_TYPE_RADIO => self::OPTION_GROUP_SELECT,
+ self::OPTION_TYPE_CHECKBOX => self::OPTION_GROUP_SELECT,
+ self::OPTION_TYPE_MULTIPLE => self::OPTION_GROUP_SELECT,
+ self::OPTION_TYPE_DATE => self::OPTION_GROUP_DATE,
+ self::OPTION_TYPE_DATE_TIME => self::OPTION_GROUP_DATE,
+ self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE,
+ ];
parent::__construct(
$context,
@@ -314,36 +363,22 @@ public function getGroupByType($type = null)
if ($type === null) {
$type = $this->getType();
}
- $optionGroupsToTypes = [
- self::OPTION_TYPE_FIELD => self::OPTION_GROUP_TEXT,
- self::OPTION_TYPE_AREA => self::OPTION_GROUP_TEXT,
- self::OPTION_TYPE_FILE => self::OPTION_GROUP_FILE,
- self::OPTION_TYPE_DROP_DOWN => self::OPTION_GROUP_SELECT,
- self::OPTION_TYPE_RADIO => self::OPTION_GROUP_SELECT,
- self::OPTION_TYPE_CHECKBOX => self::OPTION_GROUP_SELECT,
- self::OPTION_TYPE_MULTIPLE => self::OPTION_GROUP_SELECT,
- self::OPTION_TYPE_DATE => self::OPTION_GROUP_DATE,
- self::OPTION_TYPE_DATE_TIME => self::OPTION_GROUP_DATE,
- self::OPTION_TYPE_TIME => self::OPTION_GROUP_DATE,
- ];
- return $optionGroupsToTypes[$type] ?? '';
+ return $this->optionTypesToGroups[$type] ?? '';
}
/**
* Group model factory
*
* @param string $type Option type
- * @return \Magento\Catalog\Model\Product\Option\Type\DefaultType
+ * @return DefaultType
* @throws LocalizedException
*/
public function groupFactory($type)
{
$group = $this->getGroupByType($type);
- if (!empty($group)) {
- return $this->optionTypeFactory->create(
- 'Magento\Catalog\Model\Product\Option\Type\\' . $this->string->upperCaseWords($group)
- );
+ if (!empty($group) && isset($this->optionGroups[$group])) {
+ return $this->optionTypeFactory->create($this->optionGroups[$group]);
}
throw new LocalizedException(__('The option type to get group instance is incorrect.'));
}
@@ -439,10 +474,12 @@ public function afterSave()
*/
public function getPrice($flag = false)
{
- if ($flag && $this->getPriceType() == self::$typePercent) {
- $basePrice = $this->getProduct()->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getValue();
- $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100);
- return $price;
+ if ($flag) {
+ return $this->calculateCustomOptionCatalogRule->execute(
+ $this->getProduct(),
+ (float)$this->getData(self::KEY_PRICE),
+ $this->getPriceType() === Value::TYPE_PERCENT
+ );
}
return $this->_getData(self::KEY_PRICE);
}
@@ -929,7 +966,7 @@ public function setExtensionAttributes(
private function getOptionRepository()
{
if (null === $this->optionRepository) {
- $this->optionRepository = \Magento\Framework\App\ObjectManager::getInstance()
+ $this->optionRepository = ObjectManager::getInstance()
->get(\Magento\Catalog\Model\Product\Option\Repository::class);
}
return $this->optionRepository;
@@ -943,7 +980,7 @@ private function getOptionRepository()
private function getMetadataPool()
{
if (null === $this->metadataPool) {
- $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance()
+ $this->metadataPool = ObjectManager::getInstance()
->get(\Magento\Framework\EntityManager\MetadataPool::class);
}
return $this->metadataPool;
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php
index c388be8b6f394..be7f1921afccf 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php
@@ -3,17 +3,26 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Catalog\Model\Product\Option\Type;
-use Magento\Framework\Exception\LocalizedException;
use Magento\Catalog\Api\Data\ProductCustomOptionInterface;
+use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface;
+use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface;
+use Magento\Catalog\Model\Product\Option;
+use Magento\Catalog\Model\Product\Option\Value;
+use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\LocalizedException;
/**
* Catalog product option default type
*
* @api
* @author Magento Core Team
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
*/
@@ -22,14 +31,14 @@ class DefaultType extends \Magento\Framework\DataObject
/**
* Option Instance
*
- * @var \Magento\Catalog\Model\Product\Option
+ * @var Option
*/
protected $_option;
/**
* Product Instance
*
- * @var \Magento\Catalog\Model\Product
+ * @var Product
*/
protected $_product;
@@ -54,27 +63,36 @@ class DefaultType extends \Magento\Framework\DataObject
*/
protected $_checkoutSession;
+ /**
+ * @var CalculateCustomOptionCatalogRule
+ */
+ private $calculateCustomOptionCatalogRule;
+
/**
* Construct
*
* @param \Magento\Checkout\Model\Session $checkoutSession
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* @param array $data
+ * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule
*/
public function __construct(
\Magento\Checkout\Model\Session $checkoutSession,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
- array $data = []
+ array $data = [],
+ CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null
) {
$this->_checkoutSession = $checkoutSession;
parent::__construct($data);
$this->_scopeConfig = $scopeConfig;
+ $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance()
+ ->get(CalculateCustomOptionCatalogRule::class);
}
/**
* Option Instance setter
*
- * @param \Magento\Catalog\Model\Product\Option $option
+ * @param Option $option
* @return $this
*/
public function setOption($option)
@@ -86,12 +104,12 @@ public function setOption($option)
/**
* Option Instance getter
*
+ * @return Option
* @throws \Magento\Framework\Exception\LocalizedException
- * @return \Magento\Catalog\Model\Product\Option
*/
public function getOption()
{
- if ($this->_option instanceof \Magento\Catalog\Model\Product\Option) {
+ if ($this->_option instanceof Option) {
return $this->_option;
}
throw new LocalizedException(__('The option instance type in options group is incorrect.'));
@@ -100,7 +118,7 @@ public function getOption()
/**
* Product Instance setter
*
- * @param \Magento\Catalog\Model\Product $product
+ * @param Product $product
* @return $this
*/
public function setProduct($product)
@@ -112,12 +130,12 @@ public function setProduct($product)
/**
* Product Instance getter
*
+ * @return Product
* @throws \Magento\Framework\Exception\LocalizedException
- * @return \Magento\Catalog\Model\Product
*/
public function getProduct()
{
- if ($this->_product instanceof \Magento\Catalog\Model\Product) {
+ if ($this->_product instanceof Product) {
return $this->_product;
}
throw new LocalizedException(__('The product instance type in options group is incorrect.'));
@@ -126,15 +144,12 @@ public function getProduct()
/**
* Getter for Configuration Item Option
*
- * @return \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface
+ * @return OptionInterface
* @throws LocalizedException
*/
public function getConfigurationItemOption()
{
- if ($this->_getData(
- 'configuration_item_option'
- ) instanceof \Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface
- ) {
+ if ($this->_getData('configuration_item_option') instanceof OptionInterface) {
return $this->_getData('configuration_item_option');
}
@@ -149,14 +164,12 @@ public function getConfigurationItemOption()
/**
* Getter for Configuration Item
*
- * @return \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface
+ * @return ItemInterface
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function getConfigurationItem()
{
- if ($this->_getData(
- 'configuration_item'
- ) instanceof \Magento\Catalog\Model\Product\Configuration\Item\ItemInterface
+ if ($this->_getData('configuration_item') instanceof ItemInterface
) {
return $this->_getData('configuration_item');
}
@@ -341,7 +354,11 @@ public function getOptionPrice($optionValue, $basePrice)
{
$option = $this->getOption();
- return $this->_getChargeableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice);
+ return $this->calculateCustomOptionCatalogRule->execute(
+ $option->getProduct(),
+ (float)$option->getPrice(),
+ $option->getPriceType() === Value::TYPE_PERCENT
+ );
}
/**
@@ -368,14 +385,14 @@ public function getProductOptions()
$options = $this->getProduct()->getOptions();
if ($options != null) {
foreach ($options as $_option) {
- /* @var $option \Magento\Catalog\Model\Product\Option */
+ /* @var $option Option */
$this->_productOptions[$this->getProduct()->getId()][$_option->getTitle()] = [
'option_id' => $_option->getId(),
];
if ($_option->getGroupByType() == ProductCustomOptionInterface::OPTION_GROUP_SELECT) {
$optionValues = [];
foreach ($_option->getValues() as $_value) {
- /* @var $value \Magento\Catalog\Model\Product\Option\Value */
+ /* @var $value Value */
$optionValues[$_value->getTitle()] = $_value->getId();
}
$this->_productOptions[$this
@@ -395,12 +412,14 @@ public function getProductOptions()
}
/**
+ * Return final chargeable price for option
+ *
* @param float $price Price of option
* @param boolean $isPercent Price type - percent or fixed
* @param float $basePrice For percent price type
* @return float
* @deprecated 102.0.4 typo in method name
- * @see _getChargeableOptionPrice
+ * @see CalculateCustomOptionCatalogRule::execute
*/
protected function _getChargableOptionPrice($price, $isPercent, $basePrice)
{
@@ -414,6 +433,8 @@ protected function _getChargableOptionPrice($price, $isPercent, $basePrice)
* @param boolean $isPercent Price type - percent or fixed
* @param float $basePrice For percent price type
* @return float
+ * @deprecated
+ * @see CalculateCustomOptionCatalogRule::execute
*/
protected function _getChargeableOptionPrice($price, $isPercent, $basePrice)
{
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php
index d2766b1bbb054..8eebd3e91c2ee 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php
@@ -3,9 +3,13 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Catalog\Model\Product\Option\Type;
+use Magento\Catalog\Model\Product\Option\Value;
+use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\LocalizedException;
/**
@@ -37,6 +41,11 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType
*/
private $singleSelectionTypes;
+ /**
+ * @var CalculateCustomOptionCatalogRule
+ */
+ private $calculateCustomOptionCatalogRule;
+
/**
* @param \Magento\Checkout\Model\Session $checkoutSession
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
@@ -44,6 +53,7 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType
* @param \Magento\Framework\Escaper $escaper
* @param array $data
* @param array $singleSelectionTypes
+ * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule
*/
public function __construct(
\Magento\Checkout\Model\Session $checkoutSession,
@@ -51,7 +61,8 @@ public function __construct(
\Magento\Framework\Stdlib\StringUtils $string,
\Magento\Framework\Escaper $escaper,
array $data = [],
- array $singleSelectionTypes = []
+ array $singleSelectionTypes = [],
+ CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null
) {
$this->string = $string;
$this->_escaper = $escaper;
@@ -61,6 +72,8 @@ public function __construct(
'drop_down' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN,
'radio' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO,
];
+ $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance()
+ ->get(CalculateCustomOptionCatalogRule::class);
}
/**
@@ -248,10 +261,10 @@ public function getOptionPrice($optionValue, $basePrice)
foreach (explode(',', $optionValue) as $value) {
$_result = $option->getValueById($value);
if ($_result) {
- $result += $this->_getChargeableOptionPrice(
- $_result->getPrice(),
- $_result->getPriceType() == 'percent',
- $basePrice
+ $result += $this->calculateCustomOptionCatalogRule->execute(
+ $option->getProduct(),
+ (float)$_result->getPrice(),
+ $_result->getPriceType() === Value::TYPE_PERCENT
);
} else {
if ($this->getListener()) {
@@ -263,10 +276,10 @@ public function getOptionPrice($optionValue, $basePrice)
} elseif ($this->_isSingleSelection()) {
$_result = $option->getValueById($optionValue);
if ($_result) {
- $result = $this->_getChargeableOptionPrice(
- $_result->getPrice(),
- $_result->getPriceType() == 'percent',
- $basePrice
+ $result = $this->calculateCustomOptionCatalogRule->execute(
+ $option->getProduct(),
+ (float)$_result->getPrice(),
+ $_result->getPriceType() === Value::TYPE_PERCENT
);
} else {
if ($this->getListener()) {
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php
index 9ffe75e513bce..71a6556dc8858 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php
@@ -10,6 +10,8 @@
/**
* Catalog product option text type
+ *
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class Text extends \Magento\Catalog\Model\Product\Option\Type\DefaultType
{
@@ -68,6 +70,7 @@ public function validateUserValue($values)
// Check maximal length limit
$maxCharacters = $option->getMaxCharacters();
+ $value = $this->normalizeNewLineSymbols($value);
if ($maxCharacters > 0 && $this->string->strlen($value) > $maxCharacters) {
$this->setIsValid(false);
throw new LocalizedException(__('The text is too long. Shorten the text and try again.'));
@@ -101,4 +104,15 @@ public function getFormattedOptionValue($value)
{
return $this->_escaper->escapeHtml($value);
}
+
+ /**
+ * Normalize newline symbols
+ *
+ * @param string $value
+ * @return string
+ */
+ private function normalizeNewLineSymbols(string $value)
+ {
+ return str_replace(["\r\n", "\n\r", "\r"], "\n", $value);
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php
index ebbc060c99edf..783bda4699792 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Value.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php
@@ -3,13 +3,16 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Catalog\Model\Product\Option;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Option;
-use Magento\Framework\Model\AbstractModel;
use Magento\Catalog\Pricing\Price\BasePrice;
+use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Model\AbstractModel;
use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator;
use Magento\Catalog\Pricing\Price\RegularPrice;
@@ -69,6 +72,11 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu
*/
private $customOptionPriceCalculator;
+ /**
+ * @var CalculateCustomOptionCatalogRule
+ */
+ private $calculateCustomOptionCatalogRule;
+
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
@@ -77,6 +85,7 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
* @param CustomOptionPriceCalculator|null $customOptionPriceCalculator
+ * @param CalculateCustomOptionCatalogRule|null $CalculateCustomOptionCatalogRule
*/
public function __construct(
\Magento\Framework\Model\Context $context,
@@ -85,11 +94,14 @@ public function __construct(
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
array $data = [],
- CustomOptionPriceCalculator $customOptionPriceCalculator = null
+ CustomOptionPriceCalculator $customOptionPriceCalculator = null,
+ CalculateCustomOptionCatalogRule $CalculateCustomOptionCatalogRule = null
) {
$this->_valueCollectionFactory = $valueCollectionFactory;
$this->customOptionPriceCalculator = $customOptionPriceCalculator
- ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class);
+ ?? ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class);
+ $this->calculateCustomOptionCatalogRule = $CalculateCustomOptionCatalogRule
+ ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class);
parent::__construct(
$context,
@@ -101,6 +113,8 @@ public function __construct(
}
/**
+ * Override parent _construct method
+ *
* @return void
*/
protected function _construct()
@@ -109,6 +123,8 @@ protected function _construct()
}
/**
+ * Add value.
+ *
* @codeCoverageIgnoreStart
* @param mixed $value
* @return $this
@@ -120,6 +136,8 @@ public function addValue($value)
}
/**
+ * Get values.
+ *
* @return array
*/
public function getValues()
@@ -128,6 +146,8 @@ public function getValues()
}
/**
+ * Set values.
+ *
* @param array $values
* @return $this
*/
@@ -138,6 +158,8 @@ public function setValues($values)
}
/**
+ * Unset values.
+ *
* @return $this
*/
public function unsetValues()
@@ -147,6 +169,8 @@ public function unsetValues()
}
/**
+ * Set option.
+ *
* @param Option $option
* @return $this
*/
@@ -157,6 +181,8 @@ public function setOption(Option $option)
}
/**
+ * Unset option.
+ *
* @return $this
*/
public function unsetOption()
@@ -166,7 +192,7 @@ public function unsetOption()
}
/**
- * Enter description here...
+ * Get option.
*
* @return Option
*/
@@ -176,6 +202,8 @@ public function getOption()
}
/**
+ * Set product.
+ *
* @param Product $product
* @return $this
*/
@@ -188,6 +216,8 @@ public function setProduct($product)
//@codeCoverageIgnoreEnd
/**
+ * Get product.
+ *
* @return Product
*/
public function getProduct()
@@ -199,7 +229,10 @@ public function getProduct()
}
/**
+ * Save values.
+ *
* @return $this
+ * @throws \Exception
*/
public function saveValues()
{
@@ -225,8 +258,9 @@ public function saveValues()
}
/**
- * Return price. If $flag is true and price is percent
- * return converted percent to price
+ * Return price.
+ *
+ * If $flag is true and price is percent return converted percent to price
*
* @param bool $flag
* @return float|int
@@ -234,7 +268,11 @@ public function saveValues()
public function getPrice($flag = false)
{
if ($flag) {
- return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE);
+ return $this->calculateCustomOptionCatalogRule->execute(
+ $this->getProduct(),
+ (float)$this->getData(self::KEY_PRICE),
+ $this->getPriceType() === self::TYPE_PERCENT
+ );
}
return $this->_getData(self::KEY_PRICE);
}
@@ -250,7 +288,7 @@ public function getRegularPrice()
}
/**
- * Enter description here...
+ * Get values collection.
*
* @param Option $option
* @return \Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection
@@ -268,6 +306,8 @@ public function getValuesCollection(Option $option)
}
/**
+ * Get values by option.
+ *
* @param array $optionIds
* @param int $option_id
* @param int $store_id
@@ -287,6 +327,8 @@ public function getValuesByOption($optionIds, $option_id, $store_id)
}
/**
+ * Delete value.
+ *
* @param int $option_id
* @return $this
*/
@@ -297,6 +339,8 @@ public function deleteValue($option_id)
}
/**
+ * Delete values.
+ *
* @param int $option_type_id
* @return $this
*/
diff --git a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php
index b96aff148e750..7b25533ff72b8 100644
--- a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php
+++ b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider.php
@@ -4,8 +4,11 @@
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Model\ProductLink;
+use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ProductLink\Converter\ConverterPool;
use Magento\Framework\Exception\NoSuchEntityException;
@@ -19,6 +22,11 @@ class CollectionProvider
*/
protected $providers;
+ /**
+ * @var MapProviderInterface[]
+ */
+ private $mapProviders;
+
/**
* @var ConverterPool
*/
@@ -27,43 +35,169 @@ class CollectionProvider
/**
* @param ConverterPool $converterPool
* @param CollectionProviderInterface[] $providers
+ * @param MapProviderInterface[] $mapProviders
*/
- public function __construct(ConverterPool $converterPool, array $providers = [])
+ public function __construct(ConverterPool $converterPool, array $providers = [], array $mapProviders = [])
{
$this->converterPool = $converterPool;
$this->providers = $providers;
+ $this->mapProviders = $mapProviders;
+ }
+
+ /**
+ * Extract link data from linked products.
+ *
+ * @param Product[] $linkedProducts
+ * @param string $type
+ * @return array
+ */
+ private function prepareList(array $linkedProducts, string $type): array
+ {
+ $converter = $this->converterPool->getConverter($type);
+ $links = [];
+ foreach ($linkedProducts as $item) {
+ $itemId = $item->getId();
+ $links[$itemId] = $converter->convert($item);
+ $links[$itemId]['position'] = $links[$itemId]['position'] ?? 0;
+ $links[$itemId]['link_type'] = $type;
+ }
+
+ return $links;
}
/**
* Get product collection by link type
*
- * @param \Magento\Catalog\Model\Product $product
+ * @param Product $product
* @param string $type
* @return array
* @throws NoSuchEntityException
*/
- public function getCollection(\Magento\Catalog\Model\Product $product, $type)
+ public function getCollection(Product $product, $type)
{
if (!isset($this->providers[$type])) {
throw new NoSuchEntityException(__("The collection provider isn't registered."));
}
$products = $this->providers[$type]->getLinkedProducts($product);
- $converter = $this->converterPool->getConverter($type);
- $sorterItems = [];
- foreach ($products as $item) {
- $itemId = $item->getId();
- $sorterItems[$itemId] = $converter->convert($item);
- $sorterItems[$itemId]['position'] = $sorterItems[$itemId]['position'] ?? 0;
+
+ $linkData = $this->prepareList($products, $type);
+ usort(
+ $linkData,
+ function (array $itemA, array $itemB): int {
+ $posA = (int)$itemA['position'];
+ $posB = (int)$itemB['position'];
+
+ return $posA <=> $posB;
+ }
+ );
+
+ return $linkData;
+ }
+
+ /**
+ * Load maps from map providers.
+ *
+ * @param array $map
+ * @param array $typeProcessors
+ * @param Product[] $products
+ * @return void
+ */
+ private function retrieveMaps(array &$map, array $typeProcessors, array $products): void
+ {
+ /**
+ * @var MapProviderInterface $processor
+ * @var string[] $types
+ */
+ foreach ($typeProcessors as $processorIndex => $types) {
+ $typeMap = $this->mapProviders[$processorIndex]->fetchMap($products, $types);
+ /**
+ * @var string $sku
+ * @var Product[][] $links
+ */
+ foreach ($typeMap as $sku => $links) {
+ $linkData = [];
+ foreach ($links as $linkType => $linkedProducts) {
+ $linkData[] = $this->prepareList($linkedProducts, $linkType);
+ }
+ if ($linkData) {
+ $existing = [];
+ if (array_key_exists($sku, $map)) {
+ $existing = $map[$sku];
+ }
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
+ $map[$sku] = array_merge($existing, ...$linkData);
+ }
+ }
+ }
+ }
+
+ /**
+ * Load links for each product separately.
+ *
+ * @param \SplObjectStorage $map
+ * @param string[] $types
+ * @param Product[] $products
+ * @return void
+ * @throws NoSuchEntityException
+ */
+ private function retrieveSingles(array &$map, array $types, array $products): void
+ {
+ foreach ($products as $product) {
+ $linkData = [];
+ foreach ($types as $type) {
+ $linkData[] = $this->getCollection($product, $type);
+ }
+ $linkData = array_filter($linkData);
+ if ($linkData) {
+ $existing = [];
+ if (array_key_exists($product->getSku(), $map)) {
+ $existing = $map[$product->getSku()];
+ }
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
+ $map[$product->getSku()] = array_merge($existing, ...$linkData);
+ }
}
+ }
- usort($sorterItems, function ($itemA, $itemB) {
- $posA = (int)$itemA['position'];
- $posB = (int)$itemB['position'];
+ /**
+ * Load map of linked product data.
+ *
+ * Link data consists of link_type, type, sku, position, extension attributes? and custom_attributes?.
+ *
+ * @param Product[] $products
+ * @param array $types Keys - string names, values - codes.
+ * @return array Keys - SKUs, values containing link data.
+ * @throws NoSuchEntityException
+ * @throws \InvalidArgumentException
+ */
+ public function getMap(array $products, array $types): array
+ {
+ if (!$types) {
+ throw new \InvalidArgumentException('Types are required');
+ }
+ $map = [];
+ $typeProcessors = [];
+ /** @var string[] $singleProcessors */
+ $singleProcessors = [];
+ //Finding map processors
+ foreach ($types as $type => $typeCode) {
+ foreach ($this->mapProviders as $i => $mapProvider) {
+ if ($mapProvider->canProcessLinkType($type)) {
+ if (!array_key_exists($i, $typeProcessors)) {
+ $typeProcessors[$i] = [];
+ }
+ $typeProcessors[$i][$type] = $typeCode;
+ continue 2;
+ }
+ }
+ //No map processor found, will process 1 by 1
+ $singleProcessors[] = $type;
+ }
- return $posA <=> $posB;
- });
+ $this->retrieveMaps($map, $typeProcessors, $products);
+ $this->retrieveSingles($map, $singleProcessors, $products);
- return $sorterItems;
+ return $map;
}
}
diff --git a/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider/LinkedMapProvider.php b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider/LinkedMapProvider.php
new file mode 100644
index 0000000000000..6be2d2e52cf23
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ProductLink/CollectionProvider/LinkedMapProvider.php
@@ -0,0 +1,232 @@
+ Product model cache key.
+ */
+ private const PRODUCT_CACHE_KEY_MAP = [
+ 'crosssell' => 'cross_sell_products',
+ 'upsell' => 'up_sell_products',
+ 'related' => 'related_products'
+ ];
+
+ /**
+ * @var Link
+ */
+ private $linkModel;
+
+ /**
+ * @var MetadataPool
+ */
+ private $metadata;
+
+ /**
+ * @var LinkedProductCollectionFactory
+ */
+ private $productCollectionFactory;
+
+ /**
+ * LinkedMapProvider constructor.
+ * @param Link $linkModel
+ * @param MetadataPool $metadataPool
+ * @param LinkedProductCollectionFactory $productCollectionFactory
+ */
+ public function __construct(
+ Link $linkModel,
+ MetadataPool $metadataPool,
+ LinkedProductCollectionFactory $productCollectionFactory
+ ) {
+ $this->linkModel = $linkModel;
+ $this->metadata = $metadataPool;
+ $this->productCollectionFactory = $productCollectionFactory;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function canProcessLinkType(string $linkType): bool
+ {
+ return in_array($linkType, self::TYPES, true);
+ }
+
+ /**
+ * Add linked products to the map.
+ *
+ * @param Product[][] $map
+ * @param string $sku
+ * @param string $type
+ * @param Product[] $linked
+ * @return void
+ */
+ private function addLinkedToMap(array &$map, string $sku, string $type, array $linked): void
+ {
+ if (!array_key_exists($sku, $map)) {
+ $map[$sku] = [];
+ }
+ if (!array_key_exists($type, $map[$sku])) {
+ $map[$sku][$type] = [];
+ }
+ $map[$sku][$type] = array_merge($map[$sku][$type], $linked);
+ }
+
+ /**
+ * Extract cached linked products from entities and find root products that do need a query.
+ *
+ * @param Product[] $products Products mapped by link field value.
+ * @param int[] $types Type requested.
+ * @param Product[][] $map Map of linked products.
+ * @return string[][] {Type name => Product link field values} map.
+ */
+ private function processCached(array $products, array $types, array &$map): array
+ {
+ /** @var string[][] $query */
+ $query = [];
+
+ foreach ($products as $productId => $product) {
+ $sku = $product->getSku();
+ foreach (array_keys($types) as $type) {
+ if (array_key_exists($type, self::PRODUCT_CACHE_KEY_MAP)
+ && $product->hasData(self::PRODUCT_CACHE_KEY_MAP[$type])
+ ) {
+ $this->addLinkedToMap($map, $sku, $type, $product->getData(self::PRODUCT_CACHE_KEY_MAP[$type]));
+ //Cached found, no need to load.
+ continue;
+ }
+
+ if (!array_key_exists($type, $query)) {
+ $query[$type] = [];
+ }
+ $query[$type][] = $productId;
+ }
+ }
+
+ return $query;
+ }
+
+ /**
+ * Load products linked to given products.
+ *
+ * @param string[][] $productIds {Type name => Product IDs (link field values)} map.
+ * @param int[] $types Type name => type ID map.
+ * @return Product[][] Type name => Product list map.
+ */
+ private function queryLinkedProducts(array $productIds, array $types): array
+ {
+ $found = [];
+ foreach ($types as $type => $typeId) {
+ if (!array_key_exists($type, $productIds)) {
+ continue;
+ }
+
+ /** @var LinkedProductCollection $collection */
+ $collection = $this->productCollectionFactory->create(['productIds' => $productIds[$type]]);
+ $this->linkModel->setLinkTypeId($typeId);
+ $collection->setLinkModel($this->linkModel);
+ $collection->setIsStrongMode();
+ $found[$type] = $collection->getItems();
+ }
+
+ return $found;
+ }
+
+ /**
+ * Cache found linked products for existing root product instances.
+ *
+ * @param Product[] $forProducts
+ * @param Product[][] $map
+ * @param int[] $linkTypesRequested Link types that were queried.
+ * @return void
+ */
+ private function cacheLinked(array $forProducts, array $map, array $linkTypesRequested): void
+ {
+ foreach ($forProducts as $product) {
+ $sku = $product->getSku();
+ if (!array_key_exists($sku, $map)) {
+ $found = [];
+ } else {
+ $found = $map[$sku];
+ }
+ foreach (array_keys($linkTypesRequested) as $linkName) {
+ if (!array_key_exists($linkName, $found)) {
+ $found[$linkName] = [];
+ }
+ }
+
+ foreach (self::PRODUCT_CACHE_KEY_MAP as $typeName => $cacheKey) {
+ if (!array_key_exists($typeName, $linkTypesRequested)) {
+ //If products were not queried for current type then moving on
+ continue;
+ }
+
+ $product->setData($cacheKey, $found[$typeName]);
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fetchMap(array $products, array $linkTypes): array
+ {
+ if (!$products || !$linkTypes) {
+ throw new \InvalidArgumentException('Products and link types are required.');
+ }
+
+ //Gathering products information
+ $productActualIdField = $this->metadata->getMetadata(ProductInterface::class)->getLinkField();
+ /** @var Product[] $rootProducts */
+ $rootProducts = [];
+ /** @var Product $product */
+ foreach ($products as $product) {
+ if ($id = $product->getData($productActualIdField)) {
+ $rootProducts[$id] = $product;
+ }
+ }
+ unset($product);
+ //Cannot load without persisted products
+ if (!$rootProducts) {
+ return [];
+ }
+
+ //Finding linked.
+ $map = [];
+ $query = $this->processCached($rootProducts, $linkTypes, $map);
+ $foundLinked = $this->queryLinkedProducts($query, $linkTypes);
+
+ //Filling map with what we've found.
+ foreach ($foundLinked as $linkType => $linkedProducts) {
+ foreach ($linkedProducts as $linkedProduct) {
+ $product = $rootProducts[$linkedProduct->getData('_linked_to_product_id')];
+ $this->addLinkedToMap($map, $product->getSku(), $linkType, [$linkedProduct]);
+ }
+ }
+
+ $this->cacheLinked($rootProducts, $map, $linkTypes);
+
+ return $map;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteria.php b/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteria.php
new file mode 100644
index 0000000000000..4ba59e1fd08e2
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteria.php
@@ -0,0 +1,77 @@
+productSku = $belongsToProductSku;
+ $this->linkTypes = $linkTypes;
+ if ($belongsToProduct) {
+ $this->productSku = $belongsToProduct->getSku();
+ $this->product = $belongsToProduct;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBelongsToProductSku(): string
+ {
+ return $this->productSku;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLinkTypes(): ?array
+ {
+ return $this->linkTypes;
+ }
+
+ /**
+ * Product model.
+ *
+ * @see getBelongsToProductSku()
+ * @return Product|null
+ */
+ public function getBelongsToProduct(): ?Product
+ {
+ return $this->product;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteriaInterface.php b/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteriaInterface.php
new file mode 100644
index 0000000000000..0291be5b9e783
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ProductLink/Data/ListCriteriaInterface.php
@@ -0,0 +1,28 @@
+result = $result;
+ $this->error = $error;
+ if ($this->result === null && $this->error === null) {
+ throw new \InvalidArgumentException('Result must either contain values or an error.');
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getResult(): ?array
+ {
+ return $this->result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getError(): ?\Throwable
+ {
+ return $this->error;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ProductLink/Data/ListResultInterface.php b/app/code/Magento/Catalog/Model/ProductLink/Data/ListResultInterface.php
new file mode 100644
index 0000000000000..f5c0454e7a542
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/ProductLink/Data/ListResultInterface.php
@@ -0,0 +1,30 @@
+linkTypeProvider = $linkTypeProvider;
+ $this->productRepository = $productRepository;
+ $this->criteriaBuilder = $criteriaBuilder;
+ $this->collectionProvider = $collectionProvider;
+ $this->productLinkFactory = $productLinkFactory;
+ $this->productLinkExtensionFactory = $productLinkExtensionFactory;
+ }
+
+ /**
+ * Extract all link types requested.
+ *
+ * @param \Magento\Catalog\Model\ProductLink\Data\ListCriteriaInterface[] $criteria
+ * @return string[]
+ */
+ private function extractRequestedLinkTypes(array $criteria): array
+ {
+ $linkTypes = $this->linkTypeProvider->getLinkTypes();
+ $linkTypesToLoad = [];
+ foreach ($criteria as $listCriteria) {
+ if ($listCriteria->getLinkTypes() === null) {
+ //All link types are to be returned.
+ $linkTypesToLoad = null;
+ break;
+ }
+ $linkTypesToLoad[] = $listCriteria->getLinkTypes();
+ }
+ if ($linkTypesToLoad !== null) {
+ if (count($linkTypesToLoad) === 1) {
+ $linkTypesToLoad = $linkTypesToLoad[0];
+ } else {
+ $linkTypesToLoad = array_merge(...$linkTypesToLoad);
+ }
+ $linkTypesToLoad = array_flip($linkTypesToLoad);
+ $linkTypes = array_filter(
+ $linkTypes,
+ function (string $code) use ($linkTypesToLoad) {
+ return array_key_exists($code, $linkTypesToLoad);
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ }
+
+ return $linkTypes;
+ }
+
+ /**
+ * Load products links were requested for.
+ *
+ * @param \Magento\Catalog\Model\ProductLink\Data\ListCriteriaInterface[] $criteria
+ * @return \Magento\Catalog\Model\Product[] Keys are SKUs.
+ */
+ private function loadProductsByCriteria(array $criteria): array
+ {
+ $products = [];
+ $skusToLoad = [];
+ foreach ($criteria as $listCriteria) {
+ if ($listCriteria instanceof ListCriteria
+ && $listCriteria->getBelongsToProduct()
+ ) {
+ $products[$listCriteria->getBelongsToProduct()->getSku()] = $listCriteria->getBelongsToProduct();
+ } else {
+ $skusToLoad[] = $listCriteria->getBelongsToProductSku();
+ }
+ }
+
+ $skusToLoad = array_filter(
+ $skusToLoad,
+ function ($sku) use ($products) {
+ return !array_key_exists($sku, $products);
+ }
+ );
+ if ($skusToLoad) {
+ $loaded = $this->productRepository->getList(
+ $this->criteriaBuilder->addFilter('sku', $skusToLoad, 'in')->create()
+ );
+ foreach ($loaded->getItems() as $product) {
+ $products[$product->getSku()] = $product;
+ }
+ }
+
+ return $products;
+ }
+
+ /**
+ * Convert links data to DTOs.
+ *
+ * @param string $productSku SKU of the root product.
+ * @param array[] $linksData Links data returned from collection.
+ * @param string[]|null $acceptedTypes Link types that are accepted.
+ * @return \Magento\Catalog\Api\Data\ProductLinkInterface[]
+ */
+ private function convertLinksData(string $productSku, array $linksData, ?array $acceptedTypes): array
+ {
+ $list = [];
+ foreach ($linksData as $linkData) {
+ if ($acceptedTypes && !in_array($linkData['link_type'], $acceptedTypes, true)) {
+ continue;
+ }
+ /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */
+ $productLink = $this->productLinkFactory->create();
+ $productLink->setSku($productSku)
+ ->setLinkType($linkData['link_type'])
+ ->setLinkedProductSku($linkData['sku'])
+ ->setLinkedProductType($linkData['type'])
+ ->setPosition($linkData['position']);
+ if (isset($linkData['custom_attributes'])) {
+ $productLinkExtension = $productLink->getExtensionAttributes();
+ if ($productLinkExtension === null) {
+ /** @var \Magento\Catalog\Api\Data\ProductLinkExtensionInterface $productLinkExtension */
+ $productLinkExtension = $this->productLinkExtensionFactory->create();
+ }
+ foreach ($linkData['custom_attributes'] as $option) {
+ $name = $option['attribute_code'];
+ $value = $option['value'];
+ $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name);
+ // Check if setter exists
+ if (method_exists($productLinkExtension, $setterName)) {
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ call_user_func([$productLinkExtension, $setterName], $value);
+ }
+ }
+ $productLink->setExtensionAttributes($productLinkExtension);
+ }
+ $list[] = $productLink;
+ }
+
+ return $list;
+ }
+
+ /**
+ * Get list of product links found by criteria.
+ *
+ * Results are returned in the same order as criteria items.
+ *
+ * @param \Magento\Catalog\Model\ProductLink\Data\ListCriteriaInterface[] $criteria
+ * @return \Magento\Catalog\Model\ProductLink\Data\ListResultInterface[]
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ public function search(array $criteria): array
+ {
+ if (!$criteria) {
+ throw InputException::requiredField('criteria');
+ }
+
+ //Requested link types.
+ $linkTypes = $this->extractRequestedLinkTypes($criteria);
+ //Requested products.
+ $products = $this->loadProductsByCriteria($criteria);
+ //Map of products and their linked products' data.
+ $map = $this->collectionProvider->getMap($products, $linkTypes);
+
+ //Batch contract results.
+ $results = [];
+ foreach ($criteria as $listCriteria) {
+ $productSku = $listCriteria->getBelongsToProductSku();
+ if (!array_key_exists($productSku, $map)) {
+ $results[] = new ListResult([], null);
+ continue;
+ }
+ try {
+ $list = $this->convertLinksData($productSku, $map[$productSku], $listCriteria->getLinkTypes());
+ $results[] = new ListResult($list, null);
+ } catch (\Throwable $error) {
+ $results[] = new ListResult(null, $error);
+ }
+ }
+
+ return $results;
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/ProductLink/Repository.php b/app/code/Magento/Catalog/Model/ProductLink/Repository.php
index 98977de7effaf..960044efbc2ec 100644
--- a/app/code/Magento/Catalog/Model/ProductLink/Repository.php
+++ b/app/code/Magento/Catalog/Model/ProductLink/Repository.php
@@ -3,6 +3,9 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
+declare(strict_types=1);
+
namespace Magento\Catalog\Model\ProductLink;
use Magento\Catalog\Api\Data\ProductInterface;
@@ -10,6 +13,7 @@
use Magento\Catalog\Api\Data\ProductLinkExtensionFactory;
use Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks as LinksInitializer;
use Magento\Catalog\Model\Product\LinkTypeProvider;
+use Magento\Catalog\Model\ProductLink\Data\ListCriteria;
use Magento\Framework\Api\SimpleDataObjectConverter;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
@@ -17,6 +21,8 @@
use Magento\Framework\App\ObjectManager;
/**
+ * Product link entity repository.
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface
@@ -48,11 +54,14 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface
/**
* @var CollectionProvider
+ * @deprecated Not used anymore.
+ * @see query
*/
protected $entityCollectionProvider;
/**
* @var LinksInitializer
+ * @deprecated Not used.
*/
protected $linkInitializer;
@@ -68,14 +77,23 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface
/**
* @var ProductLinkInterfaceFactory
+ * @deprecated Not used anymore, search delegated.
+ * @see getList()
*/
protected $productLinkFactory;
/**
* @var ProductLinkExtensionFactory
+ * @deprecated Not used anymore, search delegated.
+ * @see getList()
*/
protected $productLinkExtensionFactory;
+ /**
+ * @var ProductLinkQuery
+ */
+ private $query;
+
/**
* Constructor
*
@@ -86,6 +104,7 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface
* @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor
* @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory|null $productLinkFactory
* @param \Magento\Catalog\Api\Data\ProductLinkExtensionFactory|null $productLinkExtensionFactory
+ * @param ProductLinkQuery|null $query
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -95,7 +114,8 @@ public function __construct(
\Magento\Catalog\Model\ProductLink\Management $linkManagement,
\Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor,
\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory $productLinkFactory = null,
- \Magento\Catalog\Api\Data\ProductLinkExtensionFactory $productLinkExtensionFactory = null
+ \Magento\Catalog\Api\Data\ProductLinkExtensionFactory $productLinkExtensionFactory = null,
+ ?ProductLinkQuery $query = null
) {
$this->productRepository = $productRepository;
$this->entityCollectionProvider = $entityCollectionProvider;
@@ -106,10 +126,11 @@ public function __construct(
->get(\Magento\Catalog\Api\Data\ProductLinkInterfaceFactory::class);
$this->productLinkExtensionFactory = $productLinkExtensionFactory ?: ObjectManager::getInstance()
->get(\Magento\Catalog\Api\Data\ProductLinkExtensionFactory::class);
+ $this->query = $query ?? ObjectManager::getInstance()->get(ProductLinkQuery::class);
}
/**
- * {@inheritdoc}
+ * @inheritDoc
*/
public function save(\Magento\Catalog\Api\Data\ProductLinkInterface $entity)
{
@@ -146,47 +167,25 @@ public function save(\Magento\Catalog\Api\Data\ProductLinkInterface $entity)
/**
* Get product links list
*
- * @param \Magento\Catalog\Api\Data\ProductInterface $product
+ * @param \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product $product
* @return \Magento\Catalog\Api\Data\ProductLinkInterface[]
*/
public function getList(\Magento\Catalog\Api\Data\ProductInterface $product)
{
- $output = [];
- $linkTypes = $this->getLinkTypeProvider()->getLinkTypes();
- foreach (array_keys($linkTypes) as $linkTypeName) {
- $collection = $this->entityCollectionProvider->getCollection($product, $linkTypeName);
- foreach ($collection as $item) {
- /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */
- $productLink = $this->productLinkFactory->create();
- $productLink->setSku($product->getSku())
- ->setLinkType($linkTypeName)
- ->setLinkedProductSku($item['sku'])
- ->setLinkedProductType($item['type'])
- ->setPosition($item['position']);
- if (isset($item['custom_attributes'])) {
- $productLinkExtension = $productLink->getExtensionAttributes();
- if ($productLinkExtension === null) {
- $productLinkExtension = $this->productLinkExtensionFactory()->create();
- }
- foreach ($item['custom_attributes'] as $option) {
- $name = $option['attribute_code'];
- $value = $option['value'];
- $setterName = 'set' . SimpleDataObjectConverter::snakeCaseToUpperCamelCase($name);
- // Check if setter exists
- if (method_exists($productLinkExtension, $setterName)) {
- call_user_func([$productLinkExtension, $setterName], $value);
- }
- }
- $productLink->setExtensionAttributes($productLinkExtension);
- }
- $output[] = $productLink;
- }
+ if (!$product->getSku() || !$product->getId()) {
+ return $product->getProductLinks();
}
- return $output;
+ $criteria = new ListCriteria($product->getSku(), null, $product);
+ $result = $this->query->search([$criteria])[0];
+
+ if ($result->getError()) {
+ throw $result->getError();
+ }
+ return $result->getResult();
}
/**
- * {@inheritdoc}
+ * @inheritDoc
*/
public function delete(\Magento\Catalog\Api\Data\ProductLinkInterface $entity)
{
@@ -219,7 +218,7 @@ public function delete(\Magento\Catalog\Api\Data\ProductLinkInterface $entity)
}
/**
- * {@inheritdoc}
+ * @inheritDoc
*/
public function deleteById($sku, $type, $linkedProductSku)
{
@@ -243,6 +242,8 @@ public function deleteById($sku, $type, $linkedProductSku)
}
/**
+ * Get Link resource instance.
+ *
* @return \Magento\Catalog\Model\ResourceModel\Product\Link
*/
private function getLinkResource()
@@ -255,6 +256,8 @@ private function getLinkResource()
}
/**
+ * Get LinkTypeProvider instance.
+ *
* @return LinkTypeProvider
*/
private function getLinkTypeProvider()
@@ -267,6 +270,8 @@ private function getLinkTypeProvider()
}
/**
+ * Get MetadataPool instance.
+ *
* @return \Magento\Framework\EntityManager\MetadataPool
*/
private function getMetadataPool()
diff --git a/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php
index fdcf2956dbdef..2aa92b8f0316e 100644
--- a/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php
+++ b/app/code/Magento/Catalog/Model/ProductRepository/MediaGalleryProcessor.php
@@ -92,7 +92,17 @@ public function processMediaGallery(ProductInterface $product, array $mediaGalle
if ($updatedEntry['file'] === null) {
unset($updatedEntry['file']);
}
- $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry);
+ if (isset($updatedEntry['content'])) {
+ //need to recreate image and reset object
+ $existingEntry['recreate'] = true;
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
+ $newEntry = array_merge($existingEntry, $updatedEntry);
+ $newEntries[] = $newEntry;
+ unset($existingMediaGallery[$key]);
+ } else {
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
+ $existingMediaGallery[$key] = array_merge($existingEntry, $updatedEntry);
+ }
} else {
//set the removed flag
$existingEntry['removed'] = true;
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
index 9e0d174a4cccb..c4980c917d069 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
@@ -771,6 +771,8 @@ public function getParentDesignCategory($category)
'custom_layout_update'
)->addAttributeToSelect(
'custom_apply_to_products'
+ )->addAttributeToSelect(
+ 'custom_layout_update_file'
)->addFieldToFilter(
'entity_id',
['in' => $pathIds]
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
index 355561c5e384d..f3984b4a35f1a 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
@@ -38,6 +38,18 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute implements
const KEY_IS_GLOBAL = 'is_global';
+ private const ALLOWED_INPUT_TYPES = [
+ 'boolean' => true,
+ 'date' => true,
+ 'datetime' => true,
+ 'multiselect' => true,
+ 'price' => true,
+ 'select' => true,
+ 'text' => true,
+ 'textarea' => true,
+ 'weight' => true,
+ ];
+
/**
* @var LockValidatorInterface
*/
@@ -236,7 +248,7 @@ public function afterSave()
) {
$this->_indexerEavProcessor->markIndexerAsInvalid();
}
-
+
$this->_source = null;
return parent::afterSave();
@@ -403,18 +415,7 @@ public function getSourceModel()
*/
public function isAllowedForRuleCondition()
{
- $allowedInputTypes = [
- 'boolean',
- 'date',
- 'datetime',
- 'multiselect',
- 'price',
- 'select',
- 'text',
- 'textarea',
- 'weight',
- ];
- return $this->getIsVisible() && in_array($this->getFrontendInput(), $allowedInputTypes);
+ return $this->getIsVisible() && isset(self::ALLOWED_INPUT_TYPES[$this->getFrontendInput()]);
}
/**
@@ -845,9 +846,6 @@ public function afterDelete()
/**
* @inheritdoc
* @since 100.0.9
- *
- * @SuppressWarnings(PHPMD.SerializationAware)
- * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -861,9 +859,6 @@ public function __sleep()
/**
* @inheritdoc
* @since 100.0.9
- *
- * @SuppressWarnings(PHPMD.SerializationAware)
- * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php
index b0b15cfd69d13..c5587d3b25665 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php
@@ -6,10 +6,12 @@
namespace Magento\Catalog\Model\ResourceModel;
use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink;
+use Magento\Eav\Api\AttributeManagementInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
use Magento\Catalog\Model\Product as ProductEntity;
use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface;
+use Magento\Framework\DataObject;
use Magento\Framework\EntityManager\EntityManager;
use Magento\Framework\Model\AbstractModel;
@@ -93,6 +95,11 @@ class Product extends AbstractResource
*/
private $tableMaintainer;
+ /**
+ * @var AttributeManagementInterface
+ */
+ private $eavAttributeManagement;
+
/**
* @param \Magento\Eav\Model\Entity\Context $context
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
@@ -106,7 +113,7 @@ class Product extends AbstractResource
* @param array $data
* @param TableMaintainer|null $tableMaintainer
* @param UniqueValidationInterface|null $uniqueValidator
- *
+ * @param AttributeManagementInterface|null $eavAttributeManagement
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -121,7 +128,8 @@ public function __construct(
\Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes,
$data = [],
TableMaintainer $tableMaintainer = null,
- UniqueValidationInterface $uniqueValidator = null
+ UniqueValidationInterface $uniqueValidator = null,
+ AttributeManagementInterface $eavAttributeManagement = null
) {
$this->_categoryCollectionFactory = $categoryCollectionFactory;
$this->_catalogCategory = $catalogCategory;
@@ -138,6 +146,8 @@ public function __construct(
);
$this->connectionName = 'catalog';
$this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class);
+ $this->eavAttributeManagement = $eavAttributeManagement
+ ?? ObjectManager::getInstance()->get(AttributeManagementInterface::class);
}
/**
@@ -268,10 +278,10 @@ public function getIdBySku($sku)
/**
* Process product data before save
*
- * @param \Magento\Framework\DataObject $object
+ * @param DataObject $object
* @return $this
*/
- protected function _beforeSave(\Magento\Framework\DataObject $object)
+ protected function _beforeSave(DataObject $object)
{
$self = parent::_beforeSave($object);
/**
@@ -286,15 +296,73 @@ protected function _beforeSave(\Magento\Framework\DataObject $object)
/**
* Save data related with product
*
- * @param \Magento\Framework\DataObject $product
+ * @param DataObject $product
* @return $this
*/
- protected function _afterSave(\Magento\Framework\DataObject $product)
+ protected function _afterSave(DataObject $product)
{
+ $this->removeNotInSetAttributeValues($product);
$this->_saveWebsiteIds($product)->_saveCategories($product);
return parent::_afterSave($product);
}
+ /**
+ * Remove attribute values that absent in product attribute set
+ *
+ * @param DataObject $product
+ * @return DataObject
+ */
+ private function removeNotInSetAttributeValues(DataObject $product): DataObject
+ {
+ $oldAttributeSetId = $product->getOrigData(ProductEntity::ATTRIBUTE_SET_ID);
+ if ($oldAttributeSetId && $product->dataHasChangedFor(ProductEntity::ATTRIBUTE_SET_ID)) {
+ $newAttributes = $product->getAttributes();
+ $newAttributesCodes = array_keys($newAttributes);
+ $oldAttributes = $this->eavAttributeManagement->getAttributes(
+ ProductEntity::ENTITY,
+ $oldAttributeSetId
+ );
+ $oldAttributesCodes = [];
+ foreach ($oldAttributes as $oldAttribute) {
+ $oldAttributesCodes[] = $oldAttribute->getAttributecode();
+ }
+ $notInSetAttributeCodes = array_diff($oldAttributesCodes, $newAttributesCodes);
+ if (!empty($notInSetAttributeCodes)) {
+ $this->deleteSelectedEntityAttributeRows($product, $notInSetAttributeCodes);
+ }
+ }
+
+ return $product;
+ }
+
+ /**
+ * Clear selected entity attribute rows
+ *
+ * @param DataObject $product
+ * @param array $attributeCodes
+ * @return void
+ */
+ private function deleteSelectedEntityAttributeRows(DataObject $product, array $attributeCodes): void
+ {
+ $backendTables = [];
+ foreach ($attributeCodes as $attributeCode) {
+ $attribute = $this->getAttribute($attributeCode);
+ $backendTable = $attribute->getBackendTable();
+ if (!$attribute->isStatic() && $backendTable) {
+ $backendTables[$backendTable][] = $attribute->getId();
+ }
+ }
+
+ $entityIdField = $this->getLinkField();
+ $entityId = $product->getData($entityIdField);
+ foreach ($backendTables as $backendTable => $attributes) {
+ $connection = $this->getConnection();
+ $where = $connection->quoteInto('attribute_id IN (?)', $attributes);
+ $where .= $connection->quoteInto(" AND {$entityIdField} = ?", $entityId);
+ $connection->delete($backendTable, $where);
+ }
+ }
+
/**
* @inheritdoc
*/
@@ -337,12 +405,12 @@ protected function _saveWebsiteIds($product)
/**
* Save product category relations
*
- * @param \Magento\Framework\DataObject $object
+ * @param DataObject $object
* @return $this
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @deprecated 101.1.0
*/
- protected function _saveCategories(\Magento\Framework\DataObject $object)
+ protected function _saveCategories(DataObject $object)
{
return $this;
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
index 442499f2da7e3..e31180d4ff6cf 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
@@ -2520,10 +2520,10 @@ public function getPricesCount()
/**
* Add is_saleable attribute to filter
*
- * @param array|null $condition
+ * @param mixed $condition
* @return $this
*/
- private function addIsSaleableAttributeToFilter(?array $condition): self
+ private function addIsSaleableAttributeToFilter($condition): self
{
$columns = $this->getSelect()->getPart(Select::COLUMNS);
foreach ($columns as $columnEntry) {
@@ -2551,10 +2551,10 @@ private function addIsSaleableAttributeToFilter(?array $condition): self
* Add tier price attribute to filter
*
* @param string $attribute
- * @param array|null $condition
+ * @param mixed $condition
* @return $this
*/
- private function addTierPriceAttributeToFilter(string $attribute, ?array $condition): self
+ private function addTierPriceAttributeToFilter(string $attribute, $condition): self
{
$attrCode = $attribute;
$connection = $this->getConnection();
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php
index 5724496d7ebdc..f1d4552cf37f0 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php
@@ -5,6 +5,14 @@
*/
namespace Magento\Catalog\Model\ResourceModel\Product\Link\Product;
+use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
+use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver;
+use Magento\Catalog\Model\ResourceModel\Category;
+use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
+use Magento\Customer\Api\GroupManagementInterface;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\Indexer\DimensionFactory;
+
/**
* Catalog product linked products collection
*
@@ -50,6 +58,111 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
*/
protected $_hasLinkFilter = false;
+ /**
+ * @var string[]|null Root product link fields values.
+ */
+ private $productIds;
+
+ /**
+ * @var string|null
+ */
+ private $linkField;
+
+ /**
+ * Collection constructor.
+ * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
+ * @param \Psr\Log\LoggerInterface $logger
+ * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
+ * @param \Magento\Framework\Event\ManagerInterface $eventManager
+ * @param \Magento\Eav\Model\Config $eavConfig
+ * @param \Magento\Framework\App\ResourceConnection $resource
+ * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory
+ * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper
+ * @param \Magento\Framework\Validator\UniversalFactory $universalFactory
+ * @param \Magento\Store\Model\StoreManagerInterface $storeManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
+ * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState
+ * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
+ * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory
+ * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl
+ * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
+ * @param \Magento\Customer\Model\Session $customerSession
+ * @param \Magento\Framework\Stdlib\DateTime $dateTime
+ * @param GroupManagementInterface $groupManagement
+ * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection
+ * @param ProductLimitationFactory|null $productLimitationFactory
+ * @param MetadataPool|null $metadataPool
+ * @param TableMaintainer|null $tableMaintainer
+ * @param PriceTableResolver|null $priceTableResolver
+ * @param DimensionFactory|null $dimensionFactory
+ * @param Category|null $categoryResourceModel
+ * @param string[]|null $productIds Root product IDs (linkFields, not entity_ids).
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
+ */
+ public function __construct(
+ \Magento\Framework\Data\Collection\EntityFactory $entityFactory,
+ \Psr\Log\LoggerInterface $logger,
+ \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
+ \Magento\Framework\Event\ManagerInterface $eventManager,
+ \Magento\Eav\Model\Config $eavConfig,
+ \Magento\Framework\App\ResourceConnection $resource,
+ \Magento\Eav\Model\EntityFactory $eavEntityFactory,
+ \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
+ \Magento\Framework\Validator\UniversalFactory $universalFactory,
+ \Magento\Store\Model\StoreManagerInterface $storeManager,
+ \Magento\Framework\Module\Manager $moduleManager,
+ \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState,
+ \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
+ \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory,
+ \Magento\Catalog\Model\ResourceModel\Url $catalogUrl,
+ \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
+ \Magento\Customer\Model\Session $customerSession,
+ \Magento\Framework\Stdlib\DateTime $dateTime,
+ GroupManagementInterface $groupManagement,
+ \Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
+ ProductLimitationFactory $productLimitationFactory = null,
+ MetadataPool $metadataPool = null,
+ TableMaintainer $tableMaintainer = null,
+ PriceTableResolver $priceTableResolver = null,
+ DimensionFactory $dimensionFactory = null,
+ Category $categoryResourceModel = null,
+ ?array $productIds = null
+ ) {
+ parent::__construct(
+ $entityFactory,
+ $logger,
+ $fetchStrategy,
+ $eventManager,
+ $eavConfig,
+ $resource,
+ $eavEntityFactory,
+ $resourceHelper,
+ $universalFactory,
+ $storeManager,
+ $moduleManager,
+ $catalogProductFlatState,
+ $scopeConfig,
+ $productOptionFactory,
+ $catalogUrl,
+ $localeDate,
+ $customerSession,
+ $dateTime,
+ $groupManagement,
+ $connection,
+ $productLimitationFactory,
+ $metadataPool,
+ $tableMaintainer,
+ $priceTableResolver,
+ $dimensionFactory,
+ $categoryResourceModel
+ );
+
+ if ($productIds) {
+ $this->productIds = $productIds;
+ $this->_hasLinkFilter = true;
+ }
+ }
+
/**
* Declare link model and initialize type attributes join
*
@@ -98,6 +211,7 @@ public function setProduct(\Magento\Catalog\Model\Product $product)
if ($product && $product->getId()) {
$this->_hasLinkFilter = true;
$this->setStore($product->getStore());
+ $this->productIds = [$product->getData($this->getLinkField())];
}
return $this;
}
@@ -142,7 +256,7 @@ public function addProductFilter($products)
if (!is_array($products)) {
$products = [$products];
}
- $identifierField = $this->getProductEntityMetadata()->getIdentifierField();
+ $identifierField = $this->getLinkField();
$this->getSelect()->where("product_entity_table.$identifierField IN (?)", $products);
$this->_hasLinkFilter = true;
}
@@ -202,21 +316,20 @@ protected function _joinLinks()
$connection->quoteInto('links.link_type_id = ?', $this->_linkTypeId),
];
$joinType = 'join';
- $linkField = $this->getProductEntityMetadata()->getLinkField();
- if ($this->getProduct() && $this->getProduct()->getId()) {
- $linkFieldId = $this->getProduct()->getData(
- $linkField
- );
+ $linkField = $this->getLinkField();
+ if ($this->productIds) {
if ($this->_isStrongMode) {
- $this->getSelect()->where('links.product_id = ?', (int)$linkFieldId);
+ $this->getSelect()->where('links.product_id in (?)', $this->productIds);
} else {
$joinType = 'joinLeft';
- $joinCondition[] = $connection->quoteInto('links.product_id = ?', $linkFieldId);
+ $joinCondition[] = $connection->quoteInto('links.product_id in (?)', $this->productIds);
+ }
+ if (count($this->productIds) === 1) {
+ $this->addFieldToFilter(
+ $linkField,
+ ['neq' => array_values($this->productIds)[0]]
+ );
}
- $this->addFieldToFilter(
- $linkField,
- ['neq' => $linkFieldId]
- );
} elseif ($this->_isStrongMode) {
$this->addFieldToFilter(
$linkField,
@@ -227,7 +340,7 @@ protected function _joinLinks()
$select->{$joinType}(
['links' => $this->getTable('catalog_product_link')],
implode(' AND ', $joinCondition),
- ['link_id']
+ ['link_id' => 'link_id', '_linked_to_product_id' => 'product_id']
);
$this->joinAttributes();
}
@@ -347,13 +460,14 @@ public function addLinkAttributeToFilter($code, $condition)
/**
* Join Product To Links
+ *
* @return void
*/
private function joinProductsToLinks()
{
if ($this->_hasLinkFilter) {
$metaDataPool = $this->getProductEntityMetadata();
- $linkField = $metaDataPool->getLinkField();
+ $linkField = $this->getLinkField();
$entityTable = $metaDataPool->getEntityTable();
$this->getSelect()
->join(
@@ -363,4 +477,18 @@ private function joinProductsToLinks()
);
}
}
+
+ /**
+ * Get product entity's identifier field.
+ *
+ * @return string
+ */
+ private function getLinkField(): string
+ {
+ if (!$this->linkField) {
+ $this->linkField = $this->getProductEntityMetadata()->getLinkField();
+ }
+
+ return $this->linkField;
+ }
}
diff --git a/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php b/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php
new file mode 100644
index 0000000000000..94977485b95b3
--- /dev/null
+++ b/app/code/Magento/Catalog/Observer/CategoryDesignAuthorization.php
@@ -0,0 +1,45 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @throws AuthorizationException
+ */
+ public function execute(Observer $observer)
+ {
+ /** @var CategoryInterface $category */
+ $category = $observer->getEvent()->getData('category');
+ $this->authorization->authorizeSavingOf($category);
+ }
+}
diff --git a/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php b/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php
new file mode 100644
index 0000000000000..af2dccb96f937
--- /dev/null
+++ b/app/code/Magento/Catalog/Plugin/CategoryAuthorization.php
@@ -0,0 +1,49 @@
+authorization = $authorization;
+ }
+
+ /**
+ * Authorize saving of a category.
+ *
+ * @param CategoryRepositoryInterface $subject
+ * @param CategoryInterface $category
+ * @throws LocalizedException
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforeSave(CategoryRepositoryInterface $subject, CategoryInterface $category): array
+ {
+ $this->authorization->authorizeSavingOf($category);
+
+ return [$category];
+ }
+}
diff --git a/app/code/Magento/Catalog/Plugin/ProductAuthorization.php b/app/code/Magento/Catalog/Plugin/ProductAuthorization.php
new file mode 100644
index 0000000000000..ce2fe19cf1aee
--- /dev/null
+++ b/app/code/Magento/Catalog/Plugin/ProductAuthorization.php
@@ -0,0 +1,53 @@
+authorization = $authorization;
+ }
+
+ /**
+ * Authorize saving of a product.
+ *
+ * @param ProductRepositoryInterface $subject
+ * @param ProductInterface $product
+ * @param bool $saveOptions
+ * @throws LocalizedException
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforeSave(
+ ProductRepositoryInterface $subject,
+ ProductInterface $product,
+ $saveOptions = false
+ ): array {
+ $this->authorization->authorizeSavingOf($product);
+
+ return [$product, $saveOptions];
+ }
+}
diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php
new file mode 100644
index 0000000000000..b3f3ac7bf68ef
--- /dev/null
+++ b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php
@@ -0,0 +1,119 @@
+priceModifier = $priceModifier;
+ $this->priceCurrency = $priceCurrency;
+ }
+
+ /**
+ * Calculate prices of custom options of the product with catalog rules applied.
+ *
+ * @param Product $product
+ * @param float $optionPriceValue
+ * @param bool $isPercent
+ * @return float
+ */
+ public function execute(
+ Product $product,
+ float $optionPriceValue,
+ bool $isPercent
+ ): float {
+ $regularPrice = (float)$product->getPriceInfo()
+ ->getPrice(RegularPrice::PRICE_CODE)
+ ->getValue();
+ $catalogRulePrice = $this->priceModifier->modifyPrice(
+ $regularPrice,
+ $product
+ );
+ $basePriceWithOutCatalogRules = (float)$this->getGetBasePriceWithOutCatalogRules($product);
+ // Apply catalog price rules to product options only if catalog price rules are applied to product.
+ if ($catalogRulePrice < $basePriceWithOutCatalogRules) {
+ $optionPrice = $this->getOptionPriceWithoutPriceRule($optionPriceValue, $isPercent, $regularPrice);
+ $totalCatalogRulePrice = $this->priceModifier->modifyPrice(
+ $regularPrice + $optionPrice,
+ $product
+ );
+ $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice;
+ } else {
+ $finalOptionPrice = $this->getOptionPriceWithoutPriceRule(
+ $optionPriceValue,
+ $isPercent,
+ $this->getGetBasePriceWithOutCatalogRules($product)
+ );
+ }
+
+ return $this->priceCurrency->convertAndRound($finalOptionPrice);
+ }
+
+ /**
+ * Get product base price without catalog rules applied.
+ *
+ * @param Product $product
+ * @return float
+ */
+ private function getGetBasePriceWithOutCatalogRules(Product $product): float
+ {
+ $basePrice = null;
+ foreach ($product->getPriceInfo()->getPrices() as $price) {
+ if ($price instanceof BasePriceProviderInterface
+ && $price->getPriceCode() !== CatalogRulePrice::PRICE_CODE
+ && $price->getValue() !== false
+ ) {
+ $basePrice = min(
+ $price->getValue(),
+ $basePrice ?? $price->getValue()
+ );
+ }
+ }
+
+ return $basePrice ?? $product->getPrice();
+ }
+
+ /**
+ * Calculate option price without catalog price rule discount.
+ *
+ * @param float $optionPriceValue
+ * @param bool $isPercent
+ * @param float $basePrice
+ * @return float
+ */
+ private function getOptionPriceWithoutPriceRule(float $optionPriceValue, bool $isPercent, float $basePrice): float
+ {
+ return $isPercent ? $basePrice * $optionPriceValue / 100 : $optionPriceValue;
+ }
+}
diff --git a/app/code/Magento/Catalog/Setup/Patch/Data/UpdateCustomLayoutAttributes.php b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateCustomLayoutAttributes.php
new file mode 100644
index 0000000000000..81fd35ccd3178
--- /dev/null
+++ b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateCustomLayoutAttributes.php
@@ -0,0 +1,121 @@
+moduleDataSetup = $moduleDataSetup;
+ $this->categorySetupFactory = $categorySetupFactory;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function getDependencies()
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAliases()
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function apply()
+ {
+ /** @var CategorySetup $eavSetup */
+ $eavSetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]);
+ $eavSetup->addAttribute(
+ Product::ENTITY,
+ 'custom_layout_update_file',
+ [
+ 'type' => 'varchar',
+ 'label' => 'Custom Layout Update',
+ 'input' => 'select',
+ 'source' => \Magento\Catalog\Model\Product\Attribute\Source\LayoutUpdate::class,
+ 'required' => false,
+ 'sort_order' => 51,
+ 'backend' => \Magento\Catalog\Model\Product\Attribute\Backend\LayoutUpdate::class,
+ 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
+ 'group' => 'Design',
+ 'is_used_in_grid' => false,
+ 'is_visible_in_grid' => false,
+ 'is_filterable_in_grid' => false
+ ]
+ );
+
+ $eavSetup->addAttribute(
+ Category::ENTITY,
+ 'custom_layout_update_file',
+ [
+ 'type' => 'varchar',
+ 'label' => 'Custom Layout Update',
+ 'input' => 'select',
+ 'source' => \Magento\Catalog\Model\Category\Attribute\Source\LayoutUpdate::class,
+ 'required' => false,
+ 'sort_order' => 51,
+ 'backend' => \Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate::class,
+ 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
+ 'group' => 'Custom Design',
+ 'is_used_in_grid' => false,
+ 'is_visible_in_grid' => false,
+ 'is_filterable_in_grid' => false
+ ]
+ );
+
+ $eavSetup->updateAttribute(
+ Product::ENTITY,
+ 'custom_layout_update',
+ 'is_visible',
+ false
+ );
+
+ $eavSetup->updateAttribute(
+ Category::ENTITY,
+ 'custom_layout_update',
+ 'is_visible',
+ false
+ );
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCategoryImageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCategoryImageActionGroup.xml
new file mode 100644
index 0000000000000..5837891667cc9
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCategoryImageActionGroup.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Requires navigation to the Category creation/edit page. Adds the provided image to a Category. Validates that the Image exists.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /magento-logo(_[0-9]+)*?\.png$/
+ grabCategoryFileName
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml
new file mode 100644
index 0000000000000..bf238a0b88417
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddCrossSellProductBySkuActionGroup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Adds the provided Product SKU as a Cross Sell Product on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductAttributeInProductModalActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductAttributeInProductModalActionGroup.xml
new file mode 100644
index 0000000000000..0b8721c589706
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductAttributeInProductModalActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Adds the provided Attribute Code to the Product on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductCustomOptionFieldActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductCustomOptionFieldActionGroup.xml
new file mode 100644
index 0000000000000..784a30de3d3c0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductCustomOptionFieldActionGroup.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Add a custom Field Product Option to a Product based on the provided Option.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductCustomOptionFileActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductCustomOptionFileActionGroup.xml
new file mode 100644
index 0000000000000..961493d06f494
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductCustomOptionFileActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ Add a custom File Product Option to a Product based on the provided File.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductImageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductImageActionGroup.xml
new file mode 100644
index 0000000000000..2efeca44003b3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductImageActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Adds the provided Product Image on the Admin Products creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml
deleted file mode 100644
index bc7288d33bcb3..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductToCartActionGroup.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
- Navigates to the Storefront Product page. Then adds the Product to the Cart. Validates that the Success Message is present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductWithQtyToCartFromStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductWithQtyToCartFromStorefrontProductPageActionGroup.xml
new file mode 100644
index 0000000000000..237db68dcf953
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddProductWithQtyToCartFromStorefrontProductPageActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ EXTENDS: addToCartFromStorefrontProductPage. Fills in the provided Product Quantity for the provided Product Name.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddRelatedProductBySkuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddRelatedProductBySkuActionGroup.xml
new file mode 100644
index 0000000000000..ce62acf6dd32a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddRelatedProductBySkuActionGroup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Adds the provided Product SKU as a Related Product on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSimpleProductToCartActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSimpleProductToCartActionGroup.xml
new file mode 100644
index 0000000000000..81e3b8c99d9d2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSimpleProductToCartActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Navigates to the Storefront Product page. Then adds the Product to the Cart. Validates that the Success Message is present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSpecialPriceToProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSpecialPriceToProductActionGroup.xml
new file mode 100644
index 0000000000000..dde25104b64dd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddSpecialPriceToProductActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Sets the provided Special Price on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddToCartFromStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddToCartFromStorefrontProductPageActionGroup.xml
new file mode 100644
index 0000000000000..b27bda2b7c8f1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddToCartFromStorefrontProductPageActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ Click on the Add to Cart button. Validates that the Success Message is present.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddUpSellProductBySkuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddUpSellProductBySkuActionGroup.xml
new file mode 100644
index 0000000000000..0ba6b479885e3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AddUpSellProductBySkuActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ EXTENDS: AddRelatedProductBySkuActionGroup. Add the provided Product as an Up Sell Product.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductActionGroup.xml
index f5e5957507eeb..958549d5768a8 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductActionGroup.xml
@@ -28,16 +28,4 @@
-
-
-
-
- EXTENDS: AdminAddAdvancedPricingToTheProductActionGroup. Removes 'selectProductTierPriceCustomerGroupInput'. Selects the provided Group Price at the provided Index for B2B.
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductExtendedActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductExtendedActionGroup.xml
new file mode 100644
index 0000000000000..cf8a3879886f1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddAdvancedPricingToTheProductExtendedActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ EXTENDS: AdminAddAdvancedPricingToTheProductActionGroup. Removes 'selectProductTierPriceCustomerGroupInput'. Selects the provided Group Price at the provided Index for B2B.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddUnassignedAttributeToGroupActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddUnassignedAttributeToGroupActionGroup.xml
new file mode 100644
index 0000000000000..2815bb7a00abf
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddUnassignedAttributeToGroupActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductCustomOptionVisibleActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductCustomOptionVisibleActionGroup.xml
new file mode 100644
index 0000000000000..c00d214d53984
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductCustomOptionVisibleActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductHasNoCustomOptionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductHasNoCustomOptionActionGroup.xml
new file mode 100644
index 0000000000000..d4d10efb697a1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductHasNoCustomOptionActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductHasNoCustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductHasNoCustomOptionsActionGroup.xml
new file mode 100644
index 0000000000000..f1a41b4922c5a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductHasNoCustomOptionsActionGroup.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignProductToCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignProductToCategoryActionGroup.xml
new file mode 100644
index 0000000000000..b88129f382263
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignProductToCategoryActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Navigates to existing product page. Changes the category and saves the product.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml
deleted file mode 100644
index afc332cc28378..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml
+++ /dev/null
@@ -1,459 +0,0 @@
-
-
-
-
-
-
-
- Requires navigation to the Category creation page. Adds a new Subcategory. Validates that the Category was created.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Category grid page. Clicks the Add Subcategory button.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Category edit page for a specified Category ID.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Requires navigation to the Subcategory creation/edit page. Fills the Subcategory Name. Fills the Search Engine Optimization.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Requires navigation to the Category creation/edit page. Checks that the url contains the AdminCategoryPage url. Saves the Category.
-
-
-
-
-
-
-
-
-
-
- Requires navigation to the Category creation/edit page. Adds the provided image to a Category. Validates that the Image exists.
-
-
-
-
-
-
-
-
-
-
-
-
-
- /magento-logo(_[0-9]+)*?\.png$/
- grabCategoryFileName
-
-
-
-
-
-
- Requires navigation to the Category creation/edit page. Removes the current Category image. Validates that the Image does not exist.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Requires navigation to the Category creation/edit page. Click on the Upload button. Validates that the Image exists.
-
-
-
-
-
-
-
-
-
-
- /magento-logo(_[0-9]+)*?\.png$/
- grabCategoryFileName
-
-
-
-
-
-
- Navigates to the category page and Opens the Media Gallery.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to the category page on the storefront and asserts that the title is correct for page and browser.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to the category page and deletes the specified category.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Actions to fill out a new category from the product page with specified category and parent category names.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Actions to delete the category last made (the last category on the list).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to category page, asserts category is there. Navigates to storefront category page and asserts category is there. This action group will not work categories where name does NOT equal SEO.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to category page, attempts to add subcategory without name. Expects required field prompt.
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to category page, selects a category and changes store view to specified store.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to category page, selects a category and changes store view to all stores.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to category page, selects a category by specified category.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Requires navigation to category creation/edit. Updates the Search Engine Optimization.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Requires navigation to subcategory creation/edit. Updates the Search Engine Optimization.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to category page, selects a category by specified category. Replicates actionGroup:navigateToCreatedCategory.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Navigates to existing product page. Changes the category and saves the product.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Requires navigation to subcategory creation/edit. Fills the name, and sets the Search Engine Optimization for the category.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Requires navigation to category creation/edit page. Assign products to category - using "Products in Category" tab.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deletes all children categories of Default Root Category.
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryAssignProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryAssignProductActionGroup.xml
new file mode 100644
index 0000000000000..454c65c8bc5c3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryAssignProductActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Requires navigation to category creation/edit page. Assign products to category - using "Products in Category" tab.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductIsMissingInCategoryProductsGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductIsMissingInCategoryProductsGridActionGroup.xml
new file mode 100644
index 0000000000000..4b360cde1485a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductIsMissingInCategoryProductsGridActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductPositionInCategoryProductsGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductPositionInCategoryProductsGridActionGroup.xml
new file mode 100644
index 0000000000000..2c0832e0ee877
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductPositionInCategoryProductsGridActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductsInGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductsInGridActionGroup.xml
deleted file mode 100644
index 440739875c0c1..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductsInGridActionGroup.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryButtonActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryButtonActionGroup.xml
new file mode 100644
index 0000000000000..33b99cef6d650
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickOnAdvancedInventoryButtonActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Clicks on the 'Advanced Inventory' link on the Admin Product creation/edit page.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeFromProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeFromProductPageActionGroup.xml
new file mode 100644
index 0000000000000..ae8e8a84149e1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeFromProductPageActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ From the Product creation/edit page, under 'Configurations', Clicks on 'Create Configurations'. Clicks on 'Create New Attribute'. Fills Label. Selects Type. Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeFromProductPageWithScopeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeFromProductPageWithScopeActionGroup.xml
new file mode 100644
index 0000000000000..2ee84fcb24b68
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeFromProductPageWithScopeActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeWithSearchWeightActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeWithSearchWeightActionGroup.xml
new file mode 100644
index 0000000000000..2ca4c9b616862
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeWithSearchWeightActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ EXTENDS: AdminProductPageCreateAttributeSetWithAttribute. Sets the provided Search Weight and Default Values.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeWithValueWithTwoStoreViesFromProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeWithValueWithTwoStoreViesFromProductPageActionGroup.xml
new file mode 100644
index 0000000000000..ec02df1fbf327
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateAttributeWithValueWithTwoStoreViesFromProductPageActionGroup.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+ EXTENDS: AdminCreateAttributeFromProductPage. Adds the 2 provided Store Views to a Product. Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCatalogProductWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCatalogProductWidgetActionGroup.xml
new file mode 100644
index 0000000000000..1b9b9d60b36e2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCatalogProductWidgetActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ EXTENDS: AdminCreateWidgetActionGroup. Creates Catalog Category Link Widget.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRecentlyProductsWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRecentlyProductsWidgetActionGroup.xml
new file mode 100644
index 0000000000000..e22620790ef70
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateRecentlyProductsWidgetActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ EXTENDS: AdminCreateWidgetActionGroup. Adds Product Attributes/Buttons to a Widget. Clicks on the Save button.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSearchableProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSearchableProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..1e1418b3e0e75
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSearchableProductAttributeActionGroup.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSimpleProductWithTextOptionCharLimitActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSimpleProductWithTextOptionCharLimitActionGroup.xml
new file mode 100644
index 0000000000000..66620e3d9dd07
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateSimpleProductWithTextOptionCharLimitActionGroup.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ Goes to the Admin Product grid page. Clicks on Add. Fills the provided Product details (Name, SKU, Price, Quantity, Category and URL). Adds a Text Product Option with the provided Char Limits. Clicks on Save. Validates that the Product details are present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml
deleted file mode 100644
index 45e2ed6205b20..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
- EXTENDS: AdminCreateWidgetActionGroup. Adds Product Attributes/Buttons to a Widget. Clicks on the Save button.
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductCustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductCustomOptionsActionGroup.xml
new file mode 100644
index 0000000000000..103c25b2c6f50
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductCustomOptionsActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCategoryByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCategoryByNameActionGroup.xml
new file mode 100644
index 0000000000000..de7f569074ac3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCategoryByNameActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteProductCustomOptionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteProductCustomOptionActionGroup.xml
new file mode 100644
index 0000000000000..22c4ef6a7d568
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteProductCustomOptionActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml
index ec73001976dc6..cd850f8a7004d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductAttributePropertiesActionGroup.xml
@@ -15,16 +15,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductCountryOfManufactureActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductCountryOfManufactureActionGroup.xml
new file mode 100644
index 0000000000000..7642bb5f6635d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillProductCountryOfManufactureActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminNavigateToProductAttributeAdvancedSectionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminNavigateToProductAttributeAdvancedSectionActionGroup.xml
new file mode 100644
index 0000000000000..27aa0474c362d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminNavigateToProductAttributeAdvancedSectionActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Navigate and open Advanced Attribute Properties section on product attribute page
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProcessProductWebsitesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProcessProductWebsitesActionGroup.xml
new file mode 100644
index 0000000000000..fbd49224231f3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProcessProductWebsitesActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
deleted file mode 100644
index 428b3828901cd..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
+++ /dev/null
@@ -1,759 +0,0 @@
-
-
-
-
-
-
-
- Clicks on the 'Add Product' toggle on the Admin Products grid page. Clicks on the provided Product (Type).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Product edit page for the provided Product ID.
-
-
-
-
-
-
-
-
-
-
-
- Fills in the provided Product details (Name, SKU, Price, Quantity, Stock Status, Weight Type and Weight) on the Admin Products creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Fills in the provided Product Name, SKU, Price, Quantity, Stock Status and Weight on the Admin Products creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Fills in the provided Product details (Name, SKU, Price, Quantity, Stock Status and Weight Type) on the Admin Products creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Fills in the provided Product details (Name and SKU) on the Admin Products creation and edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the 'Required Field' error message is present and correct for the Product Name, SKU and Price fields.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Clicks on the Save button. Validates that the Success Message is present and correct.
-
-
-
-
-
-
-
-
-
-
-
- Clicks on the Enable Product toggle.
-
-
-
-
-
-
-
-
- EXTENDS: saveProductForm. Removes 'waitProductSaveSuccessMessage' and 'seeSaveConfirmation'.
-
-
-
-
-
-
-
-
-
- Adds the provided Product Image on the Admin Products creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Removes a Product Image on the Admin Products creation/edit page.
-
-
-
-
-
-
-
-
-
-
- Removes a Product Image on the Admin Products creation/edit page by name.
-
-
-
-
-
-
-
-
-
-
-
- Validates that the provided Product Image is present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the provided Product Image is NOT present.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Product grid page. Clicks on Add. Fills the provided Product details (Name, SKU, Price, Quantity, Category and URL). Clicks on Save. Validates that the Product details are present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Product grid page. Clicks on Add. Fills the provided Product details (Name, SKU, Price, Quantity, Category and URL). Adds a Text Product Option with the provided Char Limits. Clicks on Save. Validates that the Product details are present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sets the provided Website on the Admin Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sets the provided Advanced Pricing on the Admin Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that provided Text appears in the provided Element on the Admin Product creation/edit page under 'Related Products, Up-Sells, and Cross-Sells' section.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Adds the provided Product SKU as a Related Product on the Admin Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Adds the provided Product SKU as a Cross Sell Product on the Admin Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sets the provided Special Price on the Admin Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sets the provided Website on the Admin Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sets the provided Special Price on the Admin Product creation/edit page. Clicks on Save.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Switches the New Store View.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Product grid page. Clicks on Add Product. Fills the provided Product Details (Name, SKU, Price, Quantity and Website). Clicks on Save.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Clicks on 'Edit' for the provided Product. Clicks on the provided Website. Clicks on Save.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the provided Product Tier Price is present and correct.
-
-
-
-
-
-
-
-
-
- {{amount}}
- $grabProductTierPriceInput
-
-
-
-
-
-
- Goes to the Admin Product grid page. Clicks on the Add Product toggle. Clicks on the provided Product Type.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Products grid page.
-
-
-
-
-
-
-
-
-
- Fills the Product details (URL) for the SEO section.
-
-
-
-
-
-
-
-
-
-
-
- Fills the Product SEO URL Key.
-
-
-
-
-
-
-
-
-
-
-
- Sets the provided Category Name for a Product on the Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Expand the provided Section Selector based on the provided dependant Section Selector.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Product grid page. Filters the Product grid based on the provided Product details (SKU). Edits the provided Product. Validates that the Product SKU is present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: addRelatedProductBySku. Add the provided Product as an Up Sell Product.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAddSpecialPriceActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAddSpecialPriceActionGroup.xml
new file mode 100644
index 0000000000000..fc0f908ff5fbe
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAddSpecialPriceActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Sets the provided Special Price on the Admin Product creation/edit page. Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAdvancedPricingNewCustomerGroupPriceActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAdvancedPricingNewCustomerGroupPriceActionGroup.xml
new file mode 100644
index 0000000000000..f465eceb8f008
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAdvancedPricingNewCustomerGroupPriceActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml
deleted file mode 100644
index 3e54574c553e3..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml
+++ /dev/null
@@ -1,373 +0,0 @@
-
-
-
-
-
-
- Goes to the Product Attributes grid page. Filters the grid based on the provided Product Attribute. Clicks on the 1st row.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Product Attributes grid page. Filters the grid based on the provided Product Attribute. Clicks on the 1st row.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- From the Product creation/edit page, under 'Configurations', Clicks on 'Create Configurations'. Clicks on 'Create New Attribute'. Fills Label. Selects Type. Clicks on Save.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: AdminCreateAttributeFromProductPage. Adds the 2 provided Store Views to a Product. Clicks on Save.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Adds the provided Product Attribute Set to a Product on the Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: AdminProductPageCreateAttributeSetWithAttribute. Sets the provided Search Weight and Default Values.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Selects the provided Attribute Set from the Admin Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
- Fills in the Attribute Name field with the provided value.
-
-
-
-
-
-
-
-
-
-
-
-
- Select the provided value for the 'Use for Promo Rule Conditions' dropdown menu. Clicks on Save. Validates that the Save message is present.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: navigateToCreatedProductAttribute. Deletes the Product Attribute. Validates that the success message is present.
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Product Attributes grid page. Filters the grid for the provided Product Attribute (Label). Deletes the Product Attribute from the grid. Validates that the Success Message is present.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Product Attributes grid page. Filters the grid for the provided Product Attribute Code. Deletes the Product Attribute from the grid. Validates that the Success Message is present.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Product Attributes grid by the provided Product Attribute Code.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Product Attributes grid by the provided Product Attribute Label.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Clicks on Save. Validates that the Success Message is present.
-
-
-
-
-
-
-
-
-
-
- Clicks on the Confirm button for the 'Product Data My Be Lost' modal.
-
-
-
-
-
-
-
-
-
- Clicks on Save. Validates that the Success Message is present.
-
-
-
-
-
-
-
-
-
-
-
- Adds the provided Attribute Code to the Product on the Admin Product creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Clicks on 'Add New Attribute'. Fills in the Attribute Details (Label, Input Type and Value Required). Clicks on Save.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: createProductAttribute. Fills in the Attribute Code and Default Value (Attribute Type: Text Field).
-
-
-
-
-
-
-
-
-
-
- EXTENDS: createProductAttribute. Fills in the Attribute Code and Default Value (Attribute Type: Date Field).
-
-
-
-
-
-
-
-
-
-
-
-
-
- Creates an Option for a Product Attribute (Attribute Type: Dropdown).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: createAttributeDropdownNthOption. Checks the 'Is Default' option.
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeMassUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeMassUpdateActionGroup.xml
index 57b180ada1536..d20b44b0162f0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeMassUpdateActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeMassUpdateActionGroup.xml
@@ -23,7 +23,7 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml
index e20b5f113a7ec..6e05fa614dfb9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSetActionGroup.xml
@@ -8,7 +8,7 @@
-
+
Assign the provided Attribute to an Attribute Set from the Attribute Sets creation/edit page.
@@ -23,118 +23,4 @@
-
-
-
- Unassign the provided Attribute from an Attribute Set from the Attribute Sets creation/edit page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Save an Attribute Set on the Attribute Set creation/edit page.
-
-
-
-
-
-
-
-
-
- Goes to the Attribute Sets grid page. Clicks on Add. Fill Name. Clicks on Save.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Attribute Sets grid page.
-
-
-
-
-
-
-
- Searches for the provided Attribute Sets Name. Clicks on the 1st row.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Searches the Attribute Sets grid page for the provided Attribute Set Name.
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Attribute Sets grid page for the provided Attribute Set Name.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deletes the provided Attribute Set Name from the Attribute Sets grid page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml
deleted file mode 100644
index 320a322fc5f8e..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml
+++ /dev/null
@@ -1,445 +0,0 @@
-
-
-
-
-
-
-
- Sets the Admin Products grid view to the 'Default View'.
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the provided Product (SKU).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the provided Product SKU.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the provided Product (Name).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the provided Product Name.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the New From Data field. PLEASE NOTE: The Start Date is Hardcoded.
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the provided Price Filter.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the 'Enabled' Status. PLEASE NOTE: The Filter is Hardcoded.
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the 'Disabled' Status. PLEASE NOTE: The Filter is Hardcoded.
-
-
-
-
-
-
-
-
-
-
-
-
- Searches the Admin Products grid for the provided Keyword.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Searches the Admin Products grid for the provided Keyword.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the provided Product (Name, SKU and Type).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the Admin Products grid by the provided Product (Name and SKU).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deletes the provided Product from the Admin Products grid page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: deleteProductUsingProductGrid. Removes 'seeProductSkuInGrid'.
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Products grid page. Deletes the provided Product SKU.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: deleteProductBySku. Deletes the provided Product Name.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Delete products by keyword
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Clicks on the 'Edit' button in the Admin Products grid by the provided Grid coordinates (X, Y).
-
-
-
-
-
-
-
-
-
-
-
-
- Filters the ID column in Descending Order.
-
-
-
-
-
-
-
-
-
- Filters the ID column in Ascending Order.
-
-
-
-
-
-
-
-
-
- Goes to the Admin Products grid page. Changes the Product Status to the provided Status. Validates that the Success Message is present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: resetProductGridToDefaultView. Adds an action to go to the Admin Products grid page.
-
-
-
-
-
-
-
-
- Goes to the Product Attributes grid. Clicks on 'Clear Filters'.
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Admin Products grid. Filters the Product grid by the provided Product SKU.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deletes all Products in the Admin Products grid.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deletes all products in Admin Products grid page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageCreateAttributeSetWithAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageCreateAttributeSetWithAttributeActionGroup.xml
new file mode 100644
index 0000000000000..bdf59b20f8e38
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageCreateAttributeSetWithAttributeActionGroup.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ Adds the provided Product Attribute Set to a Product on the Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageFillTextAttributeValueByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageFillTextAttributeValueByNameActionGroup.xml
new file mode 100644
index 0000000000000..336ab90ccec73
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageFillTextAttributeValueByNameActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Fills in the Attribute Name field with the provided value.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageSelectAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageSelectAttributeSetActionGroup.xml
new file mode 100644
index 0000000000000..0b58c65386951
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductPageSelectAttributeSetActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Selects the provided Attribute Set from the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductDisabledActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductDisabledActionGroup.xml
new file mode 100644
index 0000000000000..a8b413d523ff0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductDisabledActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSwitchScopeForProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSwitchScopeForProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..bd2414ae6e6c5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSwitchScopeForProductAttributeActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductStockStatusActionGroup.xml
new file mode 100644
index 0000000000000..887845b1b51a5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductStockStatusActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertDiscountsPercentageOfProductsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertDiscountsPercentageOfProductsActionGroup.xml
new file mode 100644
index 0000000000000..e32c423fb09a3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertDiscountsPercentageOfProductsActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Validates that the provided Product Tier Price is present and correct.
+
+
+
+
+
+
+
+
+
+ {{amount}}
+ $grabProductTierPriceInput
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributeRemovedSuccessfullyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributeRemovedSuccessfullyActionGroup.xml
new file mode 100644
index 0000000000000..60f1e59f9871a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductAttributeRemovedSuccessfullyActionGroup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductDescriptionInProductEditFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductDescriptionInProductEditFormActionGroup.xml
new file mode 100644
index 0000000000000..f1bca0c36a57e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductDescriptionInProductEditFormActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageAdminProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageAdminProductPageActionGroup.xml
new file mode 100644
index 0000000000000..6ea154d2b01d3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageAdminProductPageActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Validates that the provided Product Image is present and correct.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInAdminProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInAdminProductPageActionGroup.xml
new file mode 100644
index 0000000000000..a761f20b4cc5f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInAdminProductPageActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Validates that the provided Product Image is NOT present.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInStorefrontProductPage2ActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInStorefrontProductPage2ActionGroup.xml
new file mode 100644
index 0000000000000..4c7fca97a078a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInStorefrontProductPage2ActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Validates that the provided Product Image is not present.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInStorefrontProductPageActionGroup.xml
new file mode 100644
index 0000000000000..72febb053046e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageNotInStorefrontProductPageActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Validates that the provided Product Image is not present.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageStorefrontProductPage2ActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageStorefrontProductPage2ActionGroup.xml
new file mode 100644
index 0000000000000..a4769e675e607
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageStorefrontProductPage2ActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Validates that the provided Product Image is present.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageStorefrontProductPageActionGroup.xml
new file mode 100644
index 0000000000000..12be0a3d199dc
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductImageStorefrontProductPageActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Validates that the provided Product Image is present.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml
index 8db64ce7c49ac..28ceb82ec6d2e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductInStorefrontProductPageActionGroup.xml
@@ -8,7 +8,7 @@
-
+
Goes to the Storefront page. Validates that the provided Product details are present.
@@ -24,36 +24,4 @@
-
-
-
- Goes to the Storefront Product page for the provided Product. Validates that the Product details are present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Goes to the Storefront Product page for the provided Product. Validates that the Product details are present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductIsAssignedToWebsiteActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductIsAssignedToWebsiteActionGroup.xml
new file mode 100644
index 0000000000000..23937af41ae51
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductIsAssignedToWebsiteActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductIsNotAssignedToWebsiteActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductIsNotAssignedToWebsiteActionGroup.xml
new file mode 100644
index 0000000000000..8963d06e9248f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductIsNotAssignedToWebsiteActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameAndSkuInStorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameAndSkuInStorefrontProductPageActionGroup.xml
new file mode 100644
index 0000000000000..23b7701010f35
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameAndSkuInStorefrontProductPageActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Goes to the Storefront Product page for the provided Product. Validates that the Product details are present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup.xml
new file mode 100644
index 0000000000000..e50e5974023f8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Goes to the Storefront Product page for the provided Product. Validates that the Product details are present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameInProductEditFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameInProductEditFormActionGroup.xml
new file mode 100644
index 0000000000000..00f940cd4c3c8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductNameInProductEditFormActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml
index 95f1cc2c23a37..a384e993405b9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnAdminGridActionGroup.xml
@@ -8,7 +8,7 @@
-
+
EXTENDS: viewProductInAdminGrid. Removes the 'clickClearFiltersAfter' step from the Action Group.
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnCategoryPageActionGroup.xml
new file mode 100644
index 0000000000000..d35d6a4778dd6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertProductOnCategoryPageActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ EXTENDS:StorefrontCheckCategorySimpleProduct. Removes 'AssertProductPrice', 'moveMouseOverProduct', 'AssertAddToCart'
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertTextInAdminProductRelatedUpSellCrossSellSectionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertTextInAdminProductRelatedUpSellCrossSellSectionActionGroup.xml
new file mode 100644
index 0000000000000..e59f923464250
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertTextInAdminProductRelatedUpSellCrossSellSectionActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Validates that provided Text appears in the provided Element on the Admin Product creation/edit page under 'Related Products, Up-Sells, and Cross-Sells' section.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertWebsiteIsAvailableInProductWebsitesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertWebsiteIsAvailableInProductWebsitesActionGroup.xml
new file mode 100644
index 0000000000000..4bd9ce0e09581
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertWebsiteIsAvailableInProductWebsitesActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertWebsiteIsNotAvailableInProductWebsitesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertWebsiteIsNotAvailableInProductWebsitesActionGroup.xml
new file mode 100644
index 0000000000000..14eb5a40b184e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertWebsiteIsNotAvailableInProductWebsitesActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CategoryPresentActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CategoryPresentActionGroup.xml
new file mode 100644
index 0000000000000..76aec793c4eca
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CategoryPresentActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Navigates to category page, asserts category is there. Navigates to storefront category page and asserts category is there. This action group will not work categories where name does NOT equal SEO.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeSeoUrlKeyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeSeoUrlKeyActionGroup.xml
new file mode 100644
index 0000000000000..7107cc2a560d1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeSeoUrlKeyActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Requires navigation to category creation/edit. Updates the Search Engine Optimization.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeSeoUrlKeyForSubCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeSeoUrlKeyForSubCategoryActionGroup.xml
new file mode 100644
index 0000000000000..42813aef05be5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeSeoUrlKeyForSubCategoryActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Requires navigation to subcategory creation/edit. Updates the Search Engine Optimization.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeStatusProductUsingProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeStatusProductUsingProductGridActionGroup.xml
new file mode 100644
index 0000000000000..b8e8556f53813
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeStatusProductUsingProductGridActionGroup.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ Goes to the Admin Products grid page. Changes the Product Status to the provided Status. Validates that the Success Message is present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeUseForPromoRuleConditionsProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeUseForPromoRuleConditionsProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..5282fe2740fc7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ChangeUseForPromoRuleConditionsProductAttributeActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Select the provided value for the 'Use for Promo Rule Conditions' dropdown menu. Clicks on Save. Validates that the Save message is present.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInMoreInformationTabActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInMoreInformationTabActionGroup.xml
new file mode 100644
index 0000000000000..548d7de79d599
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeInMoreInformationTabActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Validates that the Product More Information area contains the provided Text.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInMoreInformationTabActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInMoreInformationTabActionGroup.xml
new file mode 100644
index 0000000000000..a6d744ebe92be
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckAttributeNotInMoreInformationTabActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validate that the More Information area does not contain the provided Text.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryImageInAdminActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryImageInAdminActionGroup.xml
new file mode 100644
index 0000000000000..fd01d4db5babb
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryImageInAdminActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Requires navigation to the Category creation/edit page. Click on the Upload button. Validates that the Image exists.
+
+
+
+
+
+
+
+
+
+
+ /magento-logo(_[0-9]+)*?\.png$/
+ grabCategoryFileName
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryNameIsRequiredFieldActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryNameIsRequiredFieldActionGroup.xml
new file mode 100644
index 0000000000000..44e1951a0463b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryNameIsRequiredFieldActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Navigates to category page, attempts to add subcategory without name. Expects required field prompt.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryOnStorefrontActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryOnStorefrontActionGroup.xml
new file mode 100644
index 0000000000000..134b3897b5e02
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCategoryOnStorefrontActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Navigates to the category page on the storefront and asserts that the title is correct for page and browser.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCustomizableOptionImportActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCustomizableOptionImportActionGroup.xml
new file mode 100644
index 0000000000000..efd0986efca06
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckCustomizableOptionImportActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Validate that the Custom Product Option Import value is present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml
index 7fbe71cbee301..6f58299fe1446 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckProductsOrderActionGroup.xml
@@ -13,12 +13,17 @@
Goes to the Storefront. Validates that the 2 provided Products appear in the correct order.
+
-
-
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckRequiredFieldsInProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckRequiredFieldsInProductFormActionGroup.xml
new file mode 100644
index 0000000000000..6aaa66dc70803
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CheckRequiredFieldsInProductFormActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Validates that the 'Required Field' error message is present and correct for the Product Name, SKU and Price fields.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ClearProductsFilterActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ClearProductsFilterActionGroup.xml
new file mode 100644
index 0000000000000..46960b5e25d51
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ClearProductsFilterActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Goto the Product grid page. Clear the Search Filters for the grid.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ConfirmChangeInputTypeModalActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ConfirmChangeInputTypeModalActionGroup.xml
new file mode 100644
index 0000000000000..d207073a01fc0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ConfirmChangeInputTypeModalActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Clicks on the Confirm button for the 'Product Data My Be Lost' modal.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeDropdownNthOptionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeDropdownNthOptionActionGroup.xml
new file mode 100644
index 0000000000000..2bf79ca980a44
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeDropdownNthOptionActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Creates an Option for a Product Attribute (Attribute Type: Dropdown).
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeDropdownNthOptionAsDefaultActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeDropdownNthOptionAsDefaultActionGroup.xml
new file mode 100644
index 0000000000000..e9e8a98ac86a6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateAttributeDropdownNthOptionAsDefaultActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ EXTENDS: createAttributeDropdownNthOption. Checks the 'Is Default' option.
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateCategoryActionGroup.xml
new file mode 100644
index 0000000000000..660b475c02ab0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateCategoryActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ Requires navigation to the Category creation page. Adds a new Subcategory. Validates that the Category was created.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateCustomRadioOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateCustomRadioOptionsActionGroup.xml
new file mode 100644
index 0000000000000..8ac87a30afbf6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateCustomRadioOptionsActionGroup.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+ Adds a custom Radio Product Option with 3 Radio Options to a Product based on the provided Options.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateDefaultAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateDefaultAttributeSetActionGroup.xml
new file mode 100644
index 0000000000000..8b95a77465b01
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateDefaultAttributeSetActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Goes to the Attribute Sets grid page. Clicks on Add. Fill Name. Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..0a0c95fdb33cc
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Clicks on 'Add New Attribute'. Fills in the Attribute Details (Label, Input Type and Value Required). Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithDateFieldActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithDateFieldActionGroup.xml
new file mode 100644
index 0000000000000..13b1bca0ad40b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithDateFieldActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ EXTENDS: createProductAttribute. Fills in the Attribute Code and Default Value (Attribute Type: Date Field).
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithDatetimeFieldActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithDatetimeFieldActionGroup.xml
new file mode 100644
index 0000000000000..a4f0d22f6edb5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithDatetimeFieldActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ EXTENDS: createProductAttribute. Fills in the Attribute Code and Default Value (Attribute Type: Date and Time Field).
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithTextFieldActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithTextFieldActionGroup.xml
new file mode 100644
index 0000000000000..fccb8ad2413f8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateProductAttributeWithTextFieldActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ EXTENDS: createProductAttribute. Fills in the Attribute Code and Default Value (Attribute Type: Text Field).
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateSimpleProductAndAddToWebsiteActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateSimpleProductAndAddToWebsiteActionGroup.xml
new file mode 100644
index 0000000000000..97c7072abe42f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreateSimpleProductAndAddToWebsiteActionGroup.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ Goes to the Admin Product grid page. Clicks on Add Product. Fills the provided Product Details (Name, SKU, Price, Quantity and Website). Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreatedProductConnectToWebsiteActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreatedProductConnectToWebsiteActionGroup.xml
new file mode 100644
index 0000000000000..d605aa1910134
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CreatedProductConnectToWebsiteActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Clicks on 'Edit' for the provided Product. Clicks on the provided Website. Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml
deleted file mode 100644
index 753f06cd75e3e..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml
+++ /dev/null
@@ -1,172 +0,0 @@
-
-
-
-
-
-
- Adds a custom Radio Product Option with 3 Radio Options to a Product based on the provided Options.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add a custom File Product Option to a Product based on the provided File.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add a custom Field Product Option to a Product based on the provided Option.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Import custom Product Options for the provided Product Name.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Click on the Reset Filters button for the Import Options filters on the Product grid page.
-
-
-
-
-
-
-
-
-
- Validate that the Custom Product Option Import value is present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAllDuplicateProductUsingProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAllDuplicateProductUsingProductGridActionGroup.xml
new file mode 100644
index 0000000000000..8f057052d3e2c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAllDuplicateProductUsingProductGridActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ EXTENDS: DeleteProductUsingProductGridActionGroup. Removes 'seeProductSkuInGrid'.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAllProductsUsingProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAllProductsUsingProductGridActionGroup.xml
new file mode 100644
index 0000000000000..3609d992a9f0b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAllProductsUsingProductGridActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Deletes all products in Admin Products grid page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetByLabelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetByLabelActionGroup.xml
new file mode 100644
index 0000000000000..c0586367fcaf6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteAttributeSetByLabelActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ Deletes the provided Attribute Set Name from the Attribute Sets grid page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteCategoryActionGroup.xml
new file mode 100644
index 0000000000000..a84e92fcbb0f5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteCategoryActionGroup.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Navigates to the category page and deletes the specified category.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteDefaultCategoryChildrenActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteDefaultCategoryChildrenActionGroup.xml
new file mode 100644
index 0000000000000..2fb4e0e05887a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteDefaultCategoryChildrenActionGroup.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ Deletes all children categories of Default Root Category.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteMostRecentCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteMostRecentCategoryActionGroup.xml
new file mode 100644
index 0000000000000..8b47b6375c21c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteMostRecentCategoryActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Actions to delete the category last made (the last category on the list).
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..8ea0e43d0307f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ EXTENDS: navigateToCreatedProductAttribute. Deletes the Product Attribute. Validates that the success message is present.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml
index 66d4378872605..ddba6649161ef 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByAttributeCodeActionGroup.xml
@@ -15,7 +15,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByLabelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByLabelActionGroup.xml
new file mode 100644
index 0000000000000..fb78909eab0b6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductAttributeByLabelActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Goes to the Admin Product Attributes grid page. Filters the grid for the provided Product Attribute (Label). Deletes the Product Attribute from the grid. Validates that the Success Message is present.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductByNameActionGroup.xml
new file mode 100644
index 0000000000000..3a4af44158497
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductByNameActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ EXTENDS: DeleteProductBySkuActionGroup. Deletes the provided Product Name.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductBySkuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductBySkuActionGroup.xml
new file mode 100644
index 0000000000000..c1f22920ceb99
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductBySkuActionGroup.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ Goes to the Admin Products grid page. Deletes the provided Product SKU.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductUsingProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductUsingProductGridActionGroup.xml
new file mode 100644
index 0000000000000..95ad1d743690e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductUsingProductGridActionGroup.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ Deletes the provided Product from the Admin Products grid page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductsByKeywordActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductsByKeywordActionGroup.xml
new file mode 100644
index 0000000000000..6b181d5f93be2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductsByKeywordActionGroup.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Delete products by keyword
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductsIfTheyExistActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductsIfTheyExistActionGroup.xml
new file mode 100644
index 0000000000000..3ef0de8b27a65
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/DeleteProductsIfTheyExistActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Deletes all Products in the Admin Products grid.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ExpandAdminProductSectionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ExpandAdminProductSectionActionGroup.xml
new file mode 100644
index 0000000000000..a181cce591e09
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ExpandAdminProductSectionActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Expand the provided Section Selector based on the provided dependant Section Selector.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillAdminSimpleProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillAdminSimpleProductFormActionGroup.xml
new file mode 100644
index 0000000000000..2abfc546e6bb3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillAdminSimpleProductFormActionGroup.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ Goes to the Admin Product grid page. Clicks on Add. Fills the provided Product details (Name, SKU, Price, Quantity, Category and URL). Clicks on Save. Validates that the Product details are present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillCategoryFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillCategoryFormActionGroup.xml
new file mode 100644
index 0000000000000..37989ed9ddc44
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillCategoryFormActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Requires navigation to the Subcategory creation/edit page. Fills the Subcategory Name. Fills the Search Engine Optimization.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillCategoryNameAndUrlKeyAndSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillCategoryNameAndUrlKeyAndSaveActionGroup.xml
new file mode 100644
index 0000000000000..bd62dd9e83e55
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillCategoryNameAndUrlKeyAndSaveActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Requires navigation to subcategory creation/edit. Fills the name, and sets the Search Engine Optimization for the category.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormActionGroup.xml
new file mode 100644
index 0000000000000..f53059b3de063
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Fills in the provided Product details (Name, SKU, Price, Quantity, Stock Status, Weight Type and Weight) on the Admin Products creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormByStringActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormByStringActionGroup.xml
new file mode 100644
index 0000000000000..4f3157292d356
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormByStringActionGroup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Fills in the provided Product Name, SKU, Price, Quantity, Stock Status and Weight on the Admin Products creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormNoWeightActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormNoWeightActionGroup.xml
new file mode 100644
index 0000000000000..b3cc110224b25
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillMainProductFormNoWeightActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Fills in the provided Product details (Name, SKU, Price, Quantity, Stock Status and Weight Type) on the Admin Products creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillNewProductCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillNewProductCategoryActionGroup.xml
new file mode 100644
index 0000000000000..9edcfca9416f2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillNewProductCategoryActionGroup.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ Actions to fill out a new category from the product page with specified category and parent category names.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillProductNameAndSkuInProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillProductNameAndSkuInProductFormActionGroup.xml
new file mode 100644
index 0000000000000..822b74d9e9e6b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FillProductNameAndSkuInProductFormActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Fills in the provided Product details (Name and SKU) on the Admin Products creation and edit page.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterAndSelectProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterAndSelectProductActionGroup.xml
new file mode 100644
index 0000000000000..2cd64ddc2855f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterAndSelectProductActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ Goes to the Admin Products grid. Filters the Product grid by the provided Product SKU.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByAttributeCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByAttributeCodeActionGroup.xml
new file mode 100644
index 0000000000000..efd1d50cb4929
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByAttributeCodeActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Filters the Product Attributes grid by the provided Product Attribute Code.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByAttributeLabelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByAttributeLabelActionGroup.xml
new file mode 100644
index 0000000000000..8d2c966de6074
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByAttributeLabelActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Searches the Attribute Sets grid page for the provided Attribute Set Name.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByDefaultLabelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByDefaultLabelActionGroup.xml
new file mode 100644
index 0000000000000..31702bfdca212
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeByDefaultLabelActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Filters the Product Attributes grid by the provided Product Attribute Label.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeSetGridByAttributeSetNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeSetGridByAttributeSetNameActionGroup.xml
new file mode 100644
index 0000000000000..c8aa8eb286fc3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductAttributeSetGridByAttributeSetNameActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Filters the Attribute Sets grid page for the provided Attribute Set Name.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByDisabledStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByDisabledStatusActionGroup.xml
new file mode 100644
index 0000000000000..52b6972ec92e6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByDisabledStatusActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the 'Disabled' Status. PLEASE NOTE: The Filter is Hardcoded.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByEnabledStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByEnabledStatusActionGroup.xml
new file mode 100644
index 0000000000000..1e4d2315bf75e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByEnabledStatusActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the 'Enabled' Status. PLEASE NOTE: The Filter is Hardcoded.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByName2ActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByName2ActionGroup.xml
new file mode 100644
index 0000000000000..850ea3e167daa
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByName2ActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the provided Product Name.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByNameActionGroup.xml
new file mode 100644
index 0000000000000..efd2f1c7be08a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByNameActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the provided Product (Name).
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByPriceRangeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByPriceRangeActionGroup.xml
new file mode 100644
index 0000000000000..d3405f977ccc4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridByPriceRangeActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the provided Price Filter.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySetNewFromDateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySetNewFromDateActionGroup.xml
new file mode 100644
index 0000000000000..db12d6f9d8440
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySetNewFromDateActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the New From Data field. PLEASE NOTE: The Start Date is Hardcoded.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySku2ActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySku2ActionGroup.xml
new file mode 100644
index 0000000000000..eb5f20b7c84e3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySku2ActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the provided Product SKU.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySkuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySkuActionGroup.xml
new file mode 100644
index 0000000000000..d216b0f976efe
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySkuActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the provided Product (SKU).
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySkuAndNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySkuAndNameActionGroup.xml
new file mode 100644
index 0000000000000..80e8e2c7c3133
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/FilterProductGridBySkuAndNameActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the provided Product (Name and SKU).
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAdminCategoryPageByIdActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAdminCategoryPageByIdActionGroup.xml
new file mode 100644
index 0000000000000..b1914118cd034
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAdminCategoryPageByIdActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Goes to the Category edit page for a specified Category ID.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeGridPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeGridPageActionGroup.xml
new file mode 100644
index 0000000000000..2b5fe9d76875c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeGridPageActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Goes to the Attribute Sets grid page.
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeSetByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeSetByNameActionGroup.xml
new file mode 100644
index 0000000000000..e732f1bd7341e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToAttributeSetByNameActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Searches for the provided Attribute Sets Name. Clicks on the 1st row.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToCreateCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToCreateCategoryPageActionGroup.xml
new file mode 100644
index 0000000000000..346f47006cf37
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToCreateCategoryPageActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Goes to the Category grid page. Clicks the Add Subcategory button.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToCreateProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToCreateProductPageActionGroup.xml
new file mode 100644
index 0000000000000..7f95765337189
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToCreateProductPageActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ Clicks on the 'Add Product' toggle on the Admin Products grid page. Clicks on the provided Product (Type).
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductCatalogPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductCatalogPageActionGroup.xml
new file mode 100644
index 0000000000000..08bf948c2223b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductCatalogPageActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Goes to the Admin Products grid page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductPageViaIDActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductPageViaIDActionGroup.xml
new file mode 100644
index 0000000000000..104ef83771e9d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToProductPageViaIDActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Goes to the Product edit page for the provided Product ID.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToSpecifiedCreateProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToSpecifiedCreateProductPageActionGroup.xml
new file mode 100644
index 0000000000000..acbe990ad4c8c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToSpecifiedCreateProductPageActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Goes to the Admin Product grid page. Clicks on the Add Product toggle. Clicks on the provided Product Type.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToStorefrontCategoryPageByParametersActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToStorefrontCategoryPageByParametersActionGroup.xml
new file mode 100644
index 0000000000000..816aeaa09ee9b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToStorefrontCategoryPageByParametersActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ Goes to the Storefront Category page using URI Search Parameters.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToSubCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToSubCategoryPageActionGroup.xml
new file mode 100644
index 0000000000000..8c35c88f151d5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/GoToSubCategoryPageActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Goes to the Storefront page. Open the Parent Category menu in the Top Nav Menu. Click on a Subcategory. Validate that the Subcategory is present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ImportProductCustomizableOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ImportProductCustomizableOptionsActionGroup.xml
new file mode 100644
index 0000000000000..2a847034add1d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ImportProductCustomizableOptionsActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Import custom Product Options for the provided Product Name.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToAndResetProductAttributeGridToDefaultViewActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToAndResetProductAttributeGridToDefaultViewActionGroup.xml
new file mode 100644
index 0000000000000..6d0091d90175b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToAndResetProductAttributeGridToDefaultViewActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Goes to the Product Attributes grid. Clicks on 'Clear Filters'.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToAndResetProductGridToDefaultViewActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToAndResetProductGridToDefaultViewActionGroup.xml
new file mode 100644
index 0000000000000..c67c0c0e61ab7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToAndResetProductGridToDefaultViewActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ EXTENDS: resetProductGridToDefaultView. Adds an action to go to the Admin Products grid page.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedCategoryActionGroup.xml
new file mode 100644
index 0000000000000..b1638909652f6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedCategoryActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Navigates to category page, selects a category by specified category.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..57ce7de7f3c0b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedProductAttributeActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Goes to the Product Attributes grid page. Filters the grid based on the provided Product Attribute. Clicks on the 1st row.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedProductEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedProductEditPageActionGroup.xml
new file mode 100644
index 0000000000000..0c9b5fc1c40e6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToCreatedProductEditPageActionGroup.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ Goes to the Admin Product grid page. Filters the Product grid based on the provided Product details (SKU). Edits the provided Product. Validates that the Product SKU is present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToEditProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToEditProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..9a348d2be8208
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToEditProductAttributeActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Goes to the Product Attributes grid page. Filters the grid based on the provided Product Attribute. Clicks on the 1st row.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToMediaGalleryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToMediaGalleryActionGroup.xml
new file mode 100644
index 0000000000000..fea652311d7c1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/NavigateToMediaGalleryActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Navigates to the category page and Opens the Media Gallery.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenCategoryFromCategoryTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenCategoryFromCategoryTreeActionGroup.xml
new file mode 100644
index 0000000000000..d172b9b24ab3e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenCategoryFromCategoryTreeActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Navigates to category page, selects a category by specified category. Replicates actionGroup:navigateToCreatedCategory.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml
index 2994533d79ed0..b8803fabda00d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductAttributeFromSearchResultInGridActionGroup.xml
@@ -16,7 +16,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductForEditByClickingRowXColumnYInProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductForEditByClickingRowXColumnYInProductGridActionGroup.xml
new file mode 100644
index 0000000000000..d5b6520cb33bb
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/OpenProductForEditByClickingRowXColumnYInProductGridActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Clicks on the 'Edit' button in the Admin Products grid by the provided Grid coordinates (X, Y).
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedPricingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedPricingActionGroup.xml
new file mode 100644
index 0000000000000..95bda64202159
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedPricingActionGroup.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ Sets the provided Advanced Pricing on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedTierFixedPricingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedTierFixedPricingActionGroup.xml
new file mode 100644
index 0000000000000..97d09fb3e1018
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetAdvancedTierFixedPricingActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetWebsiteActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetWebsiteActionGroup.xml
new file mode 100644
index 0000000000000..a2439a34bc7be
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ProductSetWebsiteActionGroup.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ Sets the provided Website on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveCategoryFromProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveCategoryFromProductActionGroup.xml
new file mode 100644
index 0000000000000..37e7d6173d3a4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveCategoryFromProductActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveCategoryImageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveCategoryImageActionGroup.xml
new file mode 100644
index 0000000000000..4febb8fd82c91
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveCategoryImageActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Requires navigation to the Category creation/edit page. Removes the current Category image. Validates that the Image does not exist.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveProductImageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveProductImageActionGroup.xml
new file mode 100644
index 0000000000000..6a56828b30308
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveProductImageActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Removes a Product Image on the Admin Products creation/edit page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveProductImageByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveProductImageByNameActionGroup.xml
new file mode 100644
index 0000000000000..fc931cbe37b25
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/RemoveProductImageByNameActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Removes a Product Image on the Admin Products creation/edit page by name.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ResetImportOptionFilterActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ResetImportOptionFilterActionGroup.xml
new file mode 100644
index 0000000000000..f73a4f4ed5e4f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ResetImportOptionFilterActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Click on the Reset Filters button for the Import Options filters on the Product grid page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ResetProductGridToDefaultViewActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ResetProductGridToDefaultViewActionGroup.xml
new file mode 100644
index 0000000000000..441fe377bd435
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ResetProductGridToDefaultViewActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ Sets the Admin Products grid view to the 'Default View'.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveAttributeSetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveAttributeSetActionGroup.xml
new file mode 100644
index 0000000000000..4e8efe0885425
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveAttributeSetActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Save an Attribute Set on the Attribute Set creation/edit page.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveCategoryFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveCategoryFormActionGroup.xml
new file mode 100644
index 0000000000000..ff6afb4aaf0e9
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveCategoryFormActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Requires navigation to the Category creation/edit page. Checks that the url contains the AdminCategoryPage url. Saves the Category.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml
new file mode 100644
index 0000000000000..e1bf2dea21318
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Clicks on Save. Validates that the Success Message is present.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml
new file mode 100644
index 0000000000000..4da8232e8405d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Clicks on Save. Validates that the Success Message is present.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormActionGroup.xml
new file mode 100644
index 0000000000000..f2524d6e68bfc
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Clicks on the Save button. Validates that the Success Message is present and correct.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormNoSuccessCheckActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormNoSuccessCheckActionGroup.xml
new file mode 100644
index 0000000000000..eb1cc33de9de8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductFormNoSuccessCheckActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ EXTENDS: saveProductForm. Removes 'waitProductSaveSuccessMessage' and 'seeSaveConfirmation'.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml
index 8b289f21f76b4..4c34feff4c943 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendActionGroup.xml
@@ -23,26 +23,4 @@
-
-
-
- Search for the provided Product Name.
-
-
-
-
-
-
-
-
-
-
-
- Goto the Product grid page. Clear the Search Filters for the grid.
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendByNameActionGroup.xml
new file mode 100644
index 0000000000000..b46a3f5818aff
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchForProductOnBackendByNameActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Search for the provided Product Name.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeyword2ActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeyword2ActionGroup.xml
new file mode 100644
index 0000000000000..bf9ca75f144b5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeyword2ActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Searches the Admin Products grid for the provided Keyword.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml
new file mode 100644
index 0000000000000..e3370864e7f61
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Searches the Admin Products grid for the provided Keyword.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SelectProductInWebsitesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SelectProductInWebsitesActionGroup.xml
new file mode 100644
index 0000000000000..bc3a865c3c365
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SelectProductInWebsitesActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Sets the provided Website on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetCategoryByNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetCategoryByNameActionGroup.xml
new file mode 100644
index 0000000000000..feca7e59e7b23
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetCategoryByNameActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Sets the provided Category Name for a Product on the Product creation/edit page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetProductUrlKeyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetProductUrlKeyActionGroup.xml
new file mode 100644
index 0000000000000..a75e8f6b764b1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetProductUrlKeyActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Fills the Product details (URL) for the SEO section.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetProductUrlKeyByStringActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetProductUrlKeyByStringActionGroup.xml
new file mode 100644
index 0000000000000..d4c654523a40b
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SetProductUrlKeyByStringActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Fills the Product SEO URL Key.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdAscendingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdAscendingActionGroup.xml
new file mode 100644
index 0000000000000..38585277476f8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdAscendingActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Filters the ID column in Ascending Order.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml
new file mode 100644
index 0000000000000..635e36c458519
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Filters the ID column in Descending Order.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddCategoryProductToCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddCategoryProductToCompareActionGroup.xml
new file mode 100644
index 0000000000000..d4678a97c5bc2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddCategoryProductToCompareActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Add a Product to the Compare Product list from a Category page.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCompareActionGroup.xml
new file mode 100644
index 0000000000000..ee3a5067449dc
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddProductToCompareActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Add a Product to the Compare Product list. Validate that the Success Message is present.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddSimpleProductWithQtyActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddSimpleProductWithQtyActionGroup.xml
new file mode 100644
index 0000000000000..273893aa49445
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAddSimpleProductWithQtyActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertActiveProductImageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertActiveProductImageActionGroup.xml
new file mode 100644
index 0000000000000..0e7da54bd4028
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertActiveProductImageActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertFotoramaImageAvailabilityActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertFotoramaImageAvailabilityActionGroup.xml
new file mode 100644
index 0000000000000..79ea22403646f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertFotoramaImageAvailabilityActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml
index ad9fa9a576c7d..d15686ec3bddc 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductImagesOnProductPageActionGroup.xml
@@ -24,12 +24,4 @@
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInRecentlyComparedWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInRecentlyComparedWidgetActionGroup.xml
new file mode 100644
index 0000000000000..43db9d96a4f50
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInRecentlyComparedWidgetActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validate that the provided Product appears in the Recently Compared Products widget.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInRecentlyOrderedWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInRecentlyOrderedWidgetActionGroup.xml
new file mode 100644
index 0000000000000..de0cb05f7c5a1
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInRecentlyOrderedWidgetActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validate that the provided Product appears in the Recently Ordered Products widget.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml
index 3caa58e16654f..81bce368a0c06 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductInWidgetActionGroup.xml
@@ -20,30 +20,4 @@
-
-
-
-
- Validate that the provided Product appears in the Recently Compared Products widget.
-
-
-
-
-
-
-
-
-
-
-
-
- Validate that the provided Product appears in the Recently Ordered Products widget.
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnCategoryPageActionGroup.xml
new file mode 100644
index 0000000000000..bc341fa09bfab
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPriceOnCategoryPageActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Validate that the provided Product Price is correct on category page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml
deleted file mode 100644
index 9393669f6e46d..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
-
-
-
- Goes to the Storefront Category page using URI Search Parameters.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validate that the Category Page parameters are present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validate that the Storefront Category is present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validate that the provided Simple Product is present and correct on a Category page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS:StorefrontCheckCategorySimpleProduct. Removes 'AssertProductPrice', 'moveMouseOverProduct', 'AssertAddToCart'
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Switch the Storefront Category view to List.
-
-
-
-
-
-
-
-
- Goes to the Storefront page. Open the Parent Category menu in the Top Nav Menu. Click on a Subcategory. Validate that the Subcategory is present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortAscendingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortAscendingActionGroup.xml
new file mode 100644
index 0000000000000..f456ca6bece55
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortAscendingActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Set Ascending Direction for sorting Products on Category page
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortDescendingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortDescendingActionGroup.xml
new file mode 100644
index 0000000000000..839260b3339c8
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortDescendingActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Set Descending Direction for sorting Products on Category page
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml
index 64dd2c97a382f..5744ddaf6c69a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml
@@ -17,16 +17,4 @@
-
-
- Set Ascending Direction for sorting Products on Category page
-
-
-
-
-
- Set Descending Direction for sorting Products on Category page
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckAddToCartButtonAbsenceActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckAddToCartButtonAbsenceActionGroup.xml
new file mode 100644
index 0000000000000..66eb61b4aa8ec
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckAddToCartButtonAbsenceActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCategoryActionGroup.xml
new file mode 100644
index 0000000000000..695ba39f5d397
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCategoryActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Validate that the Storefront Category is present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCategorySimpleProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCategorySimpleProductActionGroup.xml
new file mode 100644
index 0000000000000..1f8234498ffa7
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCategorySimpleProductActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Validate that the provided Simple Product is present and correct on a Category page.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCompareSidebarProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCompareSidebarProductActionGroup.xml
new file mode 100644
index 0000000000000..220fe29337d5a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCompareSidebarProductActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Validate that the Product Name is present and correct in the Compare Product list.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCompareSimpleProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCompareSimpleProductActionGroup.xml
new file mode 100644
index 0000000000000..d7515d19ffbbd
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckCompareSimpleProductActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Validate that the Simple Product is present and correct in the Compare Product area.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup.xml
index 01751a32d2e06..a386d81f31999 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup.xml
@@ -15,11 +15,4 @@
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPositionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPositionActionGroup.xml
new file mode 100644
index 0000000000000..ce92966eb1fbf
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPositionActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml
index ac33727564505..fd843a68b9720 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckProductPriceInCategoryActionGroup.xml
@@ -9,7 +9,7 @@
-
+
EXTENDS: StorefrontCheckCategorySimpleProduct. Removes 'AssertProductPrice'. Validates that the provided Product Price is present and correct on a Storefront Product page.
@@ -17,14 +17,4 @@
-
-
- Validate that the provided Product Price is correct on category page.
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckSimpleProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckSimpleProductActionGroup.xml
new file mode 100644
index 0000000000000..a2ebf93805819
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckSimpleProductActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ Validates that the provided Simple Product information is present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClearCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClearCompareActionGroup.xml
new file mode 100644
index 0000000000000..995bf7efd3f39
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClearCompareActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Clear the Compare Products list. Validate that the Compare Products list is empty.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml
deleted file mode 100644
index b10b74c919918..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCompareActionGroup.xml
+++ /dev/null
@@ -1,95 +0,0 @@
-
-
-
-
-
-
-
- Add a Product to the Compare Product list from a Category page.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add a Product to the Compare Product list. Validate that the Success Message is present.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validate that the Product Name is present and correct in the Compare Product list.
-
-
-
-
-
-
-
-
-
-
-
- Open the Storefront Compare Product page. Validate that the Compare Product fields are present.
-
-
-
-
-
-
-
-
-
-
-
-
- Validate that the Simple Product is present and correct in the Compare Product area.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Clear the Compare Products list. Validate that the Compare Products list is empty.
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml
index e8be0db38fe2c..080f3264c037b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToCategoryPageActionGroup.xml
@@ -17,11 +17,4 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToSubCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToSubCategoryPageActionGroup.xml
new file mode 100644
index 0000000000000..71c51a9b9f567
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontGoToSubCategoryPageActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenAndCheckComparisionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenAndCheckComparisionActionGroup.xml
new file mode 100644
index 0000000000000..1bb58d15bc096
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenAndCheckComparisionActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Open the Storefront Compare Product page. Validate that the Compare Product fields are present.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml
index e0229906ad558..899603aa27d75 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageActionGroup.xml
@@ -18,15 +18,4 @@
-
-
- Goes to the Storefront Product page for the provided store code and Product URL.
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageOnSecondStoreActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageOnSecondStoreActionGroup.xml
new file mode 100644
index 0000000000000..00956e2099085
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontOpenProductPageOnSecondStoreActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Goes to the Storefront Product page for the provided store code and Product URL.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml
deleted file mode 100644
index 403b5b853d80c..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductActionGroup.xml
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
- Validates that the provided Simple Product information is present and correct.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the provided Product Image is present.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the provided Product Image is present.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the provided Product Image is not present.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the provided Product Image is not present.
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageActionGroup.xml
deleted file mode 100644
index c960f14b1cd48..0000000000000
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageActionGroup.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
-
-
-
- Click on the Add to Cart button. Validates that the Success Message is present.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EXTENDS: addToCartFromStorefrontProductPage. Fills in the provided Product Quantity for the provided Product Name.
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the Product Text Option Text Length Hint displays the correct Count for multiple inputs based on the provided Character Limit.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validates that the Product More Information area contains the provided Text.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Validate that the More Information area does not contain the provided Text.
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchCategoryViewToListModeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchCategoryViewToListModeActionGroup.xml
new file mode 100644
index 0000000000000..7e549294ecfe0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchCategoryViewToListModeActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Switch the Storefront Category view to List.
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchCategoryStoreViewActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchCategoryStoreViewActionGroup.xml
new file mode 100644
index 0000000000000..e75a63581fb3c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchCategoryStoreViewActionGroup.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ Navigates to category page, selects a category and changes store view to specified store.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchCategoryToAllStoreViewActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchCategoryToAllStoreViewActionGroup.xml
new file mode 100644
index 0000000000000..28ca577bb98f5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchCategoryToAllStoreViewActionGroup.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Navigates to category page, selects a category and changes store view to all stores.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchToTheNewStoreViewActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchToTheNewStoreViewActionGroup.xml
new file mode 100644
index 0000000000000..9dd1a2dc56d1e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SwitchToTheNewStoreViewActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Switches the New Store View.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/TestDynamicValidationHintActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/TestDynamicValidationHintActionGroup.xml
new file mode 100644
index 0000000000000..9e3f46e0400b2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/TestDynamicValidationHintActionGroup.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ Validates that the Product Text Option Text Length Hint displays the correct Count for multiple inputs based on the provided Character Limit.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ToggleProductEnabledActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ToggleProductEnabledActionGroup.xml
new file mode 100644
index 0000000000000..220d254f70c97
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ToggleProductEnabledActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Clicks on the Enable Product toggle.
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/UnassignAttributeFromGroupActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/UnassignAttributeFromGroupActionGroup.xml
new file mode 100644
index 0000000000000..d58fa1e71061d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/UnassignAttributeFromGroupActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Unassign the provided Attribute from an Attribute Set from the Attribute Sets creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/UnassignWebsiteFromProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/UnassignWebsiteFromProductActionGroup.xml
new file mode 100644
index 0000000000000..cee17cbc4b45e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/UnassignWebsiteFromProductActionGroup.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyCategoryPageParametersActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyCategoryPageParametersActionGroup.xml
new file mode 100644
index 0000000000000..dff3ca46640df
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/VerifyCategoryPageParametersActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Validate that the Category Page parameters are present and correct.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ViewProductInAdminGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ViewProductInAdminGridActionGroup.xml
new file mode 100644
index 0000000000000..9e9c7bc323eb3
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/ViewProductInAdminGridActionGroup.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Filters the Admin Products grid by the provided Product (Name, SKU and Type).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
index 6614fa4b5dbeb..e4a91902cb79b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
@@ -108,6 +108,10 @@
true
ProductAttributeFrontendLabel
+
+ multiselect
+ Multiple Select
+
attribute
select
@@ -309,6 +313,10 @@
date
No
+
+ datetime
+ No
+
date
No
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml
index bb0e85bcbb40b..a8646a58ae39c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml
@@ -86,6 +86,14 @@
White
white
+
+
+ Blue
+ false
+ 3
+ Option11Store0
+ Option11Store1
+
option1
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
index aad43bb7011c1..46bb6e527608f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
@@ -39,6 +39,10 @@
Pursuit Lumaflex™ Tone Band
x™
+
+ 25
+ EavStock25
+
100
@@ -64,6 +68,23 @@
EavStockItem
CustomAttributeCategoryIds
+
+ SimpleProduct -+~/\\<>\’“:*\$#@()!,.?`=%&^
+
+
+ simpleProduct
+ simple
+ 4
+ SimpleProduct
+ 123.00
+ 4
+ 1
+ 1000
+ simpleProduct
+ 1
+ EavStockItem
+ CustomAttributeCategoryIds
+
SimpleProductForTest1
simple
@@ -507,6 +528,11 @@
ProductOptionDateTime
ProductOptionTime
+
+
+ magento.jpg
+ ProductOptionDateTime
+
ProductOptionRadiobuttonWithTwoFixedOptions
@@ -1068,6 +1094,10 @@
BBB Product
+
+ Test_Product
+ test_sku
+
Product "!@#$%^&*()+:;\|}{][?=~`
|}{][?=~`
@@ -1303,4 +1333,9 @@
16.00
+
+
+ ProductOptionField
+ ProductOptionField2
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml
index 5a6a0b5dd9518..2576d8cdd7022 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml
@@ -26,4 +26,7 @@
Qty_777
+
+ Qty_25
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml
index 720087917aad4..404a4771a0e73 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml
@@ -11,6 +11,7 @@
OptionField
+ OptionField
field
true
1
@@ -21,6 +22,7 @@
OptionField2
+ OptionField2
field
true
1
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml
index 32f4dc1404dd7..bef0b9f56eab6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml
@@ -40,4 +40,8 @@
777
true
+
+ 25
+ true
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml
index 0e51995ac72e8..dcd7fde92283c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml
@@ -72,4 +72,12 @@
1
Red
+
+ 0
+ Blue
+
+
+ 1
+ Blue
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml
index 18564ff101fd9..291ff115302f2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml
@@ -32,4 +32,15 @@
All Pages
Sidebar Additional
+
+ Catalog Category Link
+ Magento Luma
+ Test Widget
+
+ - All Store Views
+
+ 0
+ All Pages
+ Main Content Area
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/image_content-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/image_content-meta.xml
index 8b12a2fbbbc5d..ac75a819548ae 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Metadata/image_content-meta.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/image_content-meta.xml
@@ -7,7 +7,7 @@
-->
+ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd">
string
string
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml
index 469c153d38b88..416e0d27a1824 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontCategoryPage.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
index af7e2786f6e8d..304c34b404ea5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml
index 462f721913b9c..0934e39dcb062 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection.xml
@@ -93,6 +93,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml
index 5329ad48c8f43..8bc9c03642c15 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml
@@ -10,5 +10,6 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml
index 6d4d5d86ef798..8802372968f65 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml
@@ -9,6 +9,7 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml
index 7388ebc8408dd..7649f6c344c3a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml
@@ -8,6 +8,7 @@
+
@@ -38,7 +39,7 @@
-
+
@@ -157,6 +158,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml
index 3b6f24c0f259d..4e86f14611c24 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml
@@ -36,5 +36,7 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
index ac7a15daf56aa..9a84f90edcfc0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
@@ -34,5 +34,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml
index 136a8ceadb89e..2ec7997f87b7e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection.xml
@@ -8,6 +8,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
index fd412d3c7dee1..631649e33b0fd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
@@ -34,6 +34,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml
index 6ed359e35ab59..d3e43d9ea2dfa 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml
@@ -17,5 +17,7 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml
index 87aab45bd8cb7..ca0c32f142852 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontWidgetsSection.xml
@@ -12,5 +12,6 @@
+
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml
index 53bb12fda4833..7c0161d443df6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml
@@ -45,10 +45,10 @@
-
+
-
+
@@ -58,17 +58,17 @@
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
index 117f094ee0607..ac1f967b66e41 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
@@ -28,32 +28,32 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
index 3f857c258924f..367827f8c0c28 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
@@ -28,30 +28,30 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml
index f657fbbdae607..0b33ef0ac0783 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoSimpleProductTest.xml
@@ -29,19 +29,19 @@
-
-
+
+
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml
index eab36bc90dc18..e89cf6f4242e7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultVideoVirtualProductTest.xml
@@ -21,13 +21,13 @@
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml
index 8ac0cfa512b03..ed9eb686d2c86 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml
@@ -22,27 +22,27 @@
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml
index 50d192a27e46d..b3e6fcd3bfb55 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml
@@ -10,7 +10,7 @@
-
+
@@ -31,27 +31,30 @@
-
-
-
+
+
+
-
+
-
-
+
+
-
-
+
+
-
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
index ee105320c5f29..edf37542fd830 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
@@ -19,17 +19,17 @@
-
+
-
+
-
+
@@ -70,11 +70,12 @@
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
index 51ef7fb77d74c..b98ca3a375a17 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
@@ -36,7 +36,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml
index 545e7c10379bf..4f1618e076642 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest.xml
@@ -32,7 +32,7 @@
-
+
@@ -51,7 +51,7 @@
-
+
@@ -60,7 +60,7 @@
-
+
@@ -79,7 +79,7 @@
-
+
@@ -110,13 +110,13 @@
-
+
-
+
@@ -132,10 +132,10 @@
-
+
-
+
@@ -169,7 +169,7 @@
-
+
@@ -239,7 +239,7 @@
-
+
@@ -291,7 +291,7 @@
-
+
@@ -309,7 +309,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml
index 4261721d36064..41b358bbf760e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml
@@ -40,14 +40,14 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml
index 88c524eff387c..9361637a0a935 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml
@@ -35,7 +35,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml
index bcfab6ccfdf1f..95620bf75b6d0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeSet.xml
@@ -35,11 +35,11 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml
index 86978a4121a43..b4381a674827d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml
@@ -145,7 +145,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
index ee8b48a94b20d..cd03d868838f3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
@@ -36,7 +36,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
index a863de2716c97..bfea4fa7557f5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml
@@ -39,7 +39,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
index 47e206768527b..5bb9cc8a080df 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
@@ -16,9 +16,6 @@
-
-
-
@@ -118,7 +115,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCloneProductWithDuplicateUrlTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCloneProductWithDuplicateUrlTest.xml
index 99adaeb522786..ee9e364758899 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCloneProductWithDuplicateUrlTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCloneProductWithDuplicateUrlTest.xml
@@ -28,10 +28,10 @@
-
+
-
+
@@ -47,16 +47,16 @@
-
+
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml
index 4d97dee56f059..b5ab36729c7fe 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminConfigureProductImagePlaceholderTest.xml
@@ -62,9 +62,9 @@
-
+
-
+
@@ -120,7 +120,7 @@
$getThumbnailPlaceholderImageSrc
{{placeholderThumbnailImage.name}}
-
+
@@ -138,7 +138,7 @@
$getThumbnailImageSrc
{{placeholderThumbnailImage.name}}
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
index 8ffab42653b49..80c20a7e0b5d9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
@@ -38,16 +38,16 @@
-
+
-
+
-
+
-
+
@@ -60,7 +60,7 @@
-
+
@@ -96,7 +96,7 @@
-
+
@@ -110,7 +110,7 @@
-
+
@@ -129,7 +129,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml
index 90cbab59bd71d..34451aba78f21 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditVirtualProductSettingsTest.xml
@@ -33,7 +33,7 @@
-
+
@@ -51,15 +51,15 @@
-
+
-
+
-
+
@@ -70,10 +70,10 @@
-
+
-
+
@@ -93,7 +93,7 @@
-
+
@@ -139,12 +139,12 @@
-
+
-
+
@@ -162,7 +162,7 @@
-
+
@@ -185,6 +185,6 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml
index 4deca73504677..12082e1daa6c3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType.xml
@@ -23,11 +23,11 @@
-
-
+
+
-
+
@@ -35,28 +35,28 @@
-
+
-
+
-
+
-
+
-
-
-
+
+
+
-
+
@@ -71,11 +71,11 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml
index d9e410a9a3009..51518dffaf87e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml
@@ -30,7 +30,7 @@
-
+
@@ -38,17 +38,17 @@
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml
index a5150a0fb7f24..ef21b53c7613b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml
@@ -26,7 +26,7 @@
-
+
@@ -35,7 +35,7 @@
-
+
@@ -43,12 +43,12 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
index a6890c2ad4905..7de1d38f097c5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
@@ -23,7 +23,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
index e8c6da476a3d6..a68a585bbf31d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml
@@ -26,7 +26,7 @@
-
+
@@ -41,7 +41,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml
index 96f945da138b0..fae1a1fa8c2e4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml
@@ -21,7 +21,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml
index c983089163f78..a695aa33079bd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml
@@ -21,7 +21,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml
index 79eec02a828f6..6a49b47b75078 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilter.xml
@@ -21,14 +21,14 @@
-
-
+
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
index 1b6c9707b0656..dac2a121d107f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
@@ -21,7 +21,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml
index a3f543e9cf32a..2d1a58764e20a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml
@@ -38,7 +38,7 @@
-
+
@@ -50,7 +50,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml
new file mode 100644
index 0000000000000..981af5b5abb4a
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml
index 525f81de6c48c..4118356b07e74 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml
@@ -21,7 +21,7 @@
-
+
@@ -60,7 +60,7 @@
-
+
@@ -69,4 +69,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
index 1bc69be642a37..557f0768c98a3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
@@ -39,7 +39,7 @@
-
+
@@ -47,12 +47,12 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml
index 37ec4e0d32528..cbef9566b2b78 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml
@@ -27,10 +27,10 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml
index 575bb56912b25..4507e1f880a86 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateProductTest.xml
@@ -32,10 +32,10 @@
-
+
-
+
@@ -45,17 +45,17 @@
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
index 21b3dba7140c0..b0e6fe87be918 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml
@@ -23,11 +23,11 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
index aa3dba85dfadf..7de37b9cb77ef 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml
@@ -23,11 +23,11 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
index 37417cd7fdb85..c7aba1fe8376f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml
@@ -23,11 +23,11 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
index 4e096b7ebb142..6bfd012bc88b2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
@@ -45,7 +45,7 @@
-
+
@@ -53,12 +53,12 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml
index 2706d00038e4b..51c4a3250d609 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml
@@ -26,8 +26,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml
index 02615ca5dd254..8fb226f5f5585 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml
@@ -38,7 +38,7 @@
-
+
@@ -55,7 +55,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml
index 3219bca233bee..a105f343d3e21 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml
@@ -29,13 +29,13 @@
-
+
-
+
@@ -68,9 +68,9 @@
-
+
-
+
@@ -85,7 +85,7 @@
-
+
@@ -93,8 +93,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
index 63a964f4b5e91..2e502f58041e6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
@@ -35,7 +35,7 @@
-
+
@@ -47,7 +47,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml
index d4d6496e018f5..63eed37b1e84f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml
@@ -34,7 +34,7 @@
-
+
@@ -45,7 +45,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml
index 713e1b7d6dfd1..7f6feaff3ed5d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSet.xml
@@ -71,10 +71,10 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml
index 291b6985bd3e5..f5e42bb84549c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest.xml
@@ -58,7 +58,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml
index 11d919ddefa2c..7a99750c00e53 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml
@@ -33,7 +33,7 @@
-
+
@@ -48,13 +48,13 @@
-
+
-
+
@@ -74,11 +74,11 @@
-
+
-
+
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml
index f98f9acc46961..2b824554b9bd4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml
@@ -23,7 +23,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml
index a7587a5ed31fe..052f6b1924e89 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest.xml
@@ -26,7 +26,7 @@
-
+
@@ -34,7 +34,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml
index 3487de656173f..bbaabffcc5ecd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml
@@ -25,7 +25,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml
new file mode 100644
index 0000000000000..0f88fa9d6abf4
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {$generateDefaultValue}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
index 94d488f216b49..5dcc23a725b84 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
@@ -27,15 +27,17 @@
-
+
+
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml
index fc7482c353136..848e765d34d70 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateTextEditorProductAttributeTest.xml
@@ -30,12 +30,12 @@
-
+
-
+
@@ -71,7 +71,7 @@
-
+
@@ -80,20 +80,20 @@
-
+
-
+
-
+
-
+
@@ -115,8 +115,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml
index 0b929eaddc96e..9db9f64396826 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml
@@ -24,10 +24,10 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
index 23f772a395a7d..fe39bb6ada2bd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
@@ -24,10 +24,10 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml
index 9055e961f889f..976f714d7b3e1 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml
@@ -24,7 +24,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml
index cbe2f40e0dd25..973ff0381584a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml
@@ -47,7 +47,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
index 0df9dd0b57545..4a305b8dfec75 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
@@ -95,10 +95,10 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml
index 3841c061c2629..3abe68a503b57 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml
@@ -41,7 +41,7 @@
-
+
@@ -49,11 +49,15 @@
-
-
+
+
+
+
+
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml
index d0036a2adea5a..4060182a9bace 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml
@@ -24,16 +24,20 @@
-
-
+
+
+
+
+
+
-
+
-
+
@@ -48,7 +52,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
index 7f6a1333b721a..dec911ec84a8d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
@@ -30,7 +30,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
index f334cbc218b7c..55c98bcc13d34 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
@@ -43,17 +43,17 @@
-
+
-
+
-
+
-
+
@@ -86,12 +86,12 @@
-
+
-
+
@@ -99,9 +99,9 @@
-
+
-
+
@@ -109,17 +109,17 @@
-
+
-
+
-
+
-
+
@@ -128,7 +128,7 @@
-
+
@@ -136,17 +136,17 @@
-
+
-
+
-
+
-
+
@@ -155,15 +155,15 @@
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
index 7c460a3dfc51e..5b8ac5157514d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
@@ -29,7 +29,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml
index c3cafb17c5eac..4f05c364fda0e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml
@@ -47,7 +47,7 @@
-
+
@@ -55,11 +55,15 @@
-
-
+
+
+
+
+
+
-
+
@@ -77,7 +81,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
index 413d53d1c3746..86f253f358532 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
@@ -30,7 +30,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml
index dab1704d50bf3..0fc2c022b81e9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml
@@ -33,7 +33,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
index 53040993beb8f..feea0930390b7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
@@ -19,14 +19,14 @@
-
+
-
+
@@ -75,7 +75,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml
index f3ec225540c75..0b230b0b8e002 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml
@@ -35,11 +35,11 @@
-
+
-
+
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml
index 5c434ecabf80d..77b719c03091e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml
@@ -60,7 +60,7 @@
-
+
@@ -73,7 +73,7 @@
-
+
@@ -85,7 +85,7 @@
-
+
@@ -131,7 +131,6 @@
userInput="$$createProduct1.name$$" stepKey="seeProductName4"/>
-
-
-
-
+
+
@@ -40,7 +40,7 @@
-
+
@@ -51,7 +51,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
index b1f00a2f51a95..76c0d7f7b931c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
@@ -15,70 +15,58 @@
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
-
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml
index 8d5121cf21461..6896b11196cf2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml
@@ -39,10 +39,10 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
index f5ad5b8079d1f..e98b145f01401 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
@@ -34,12 +34,12 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml
index 87e0bf3d2e9a0..71873fe5b0960 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml
@@ -16,14 +16,12 @@
-
-
-
-
-
+
+
+
@@ -38,16 +36,18 @@
-
+
+
+
-
+
-
+
@@ -58,36 +58,38 @@
-
+
-
+
+
-
-
-
-
+
+
+
-
+
+
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml
index fe0e46369c5e6..a4c8f5e8cff6c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml
@@ -39,7 +39,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml
index 18d4b9e341cc6..c9840fe455f84 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest.xml
@@ -38,10 +38,10 @@
-
+
-
+
@@ -114,10 +114,10 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml
index 02e8157282dee..21c6c56adfd96 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest.xml
@@ -55,13 +55,13 @@
-
+
-
+
@@ -86,10 +86,10 @@
-
+
-
+
@@ -205,13 +205,13 @@
-
+
-
+
@@ -236,10 +236,10 @@
-
+
-
+
@@ -299,4 +299,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
index 247711295a555..a8a8ede297b44 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
@@ -69,6 +69,10 @@
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
index b613068893b0e..271d78ab9cdb0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
@@ -25,7 +25,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
index 9831f73e07877..6c403fc7714eb 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
@@ -25,7 +25,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
index da985fc2ce34d..7e6e79cd08c26 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml
@@ -94,7 +94,7 @@
-
+
@@ -159,16 +159,16 @@
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
index bcd4ca8531203..5dd8b2e430941 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
@@ -97,7 +97,7 @@
-
+
@@ -106,7 +106,7 @@
-
+
@@ -114,7 +114,7 @@
-
+
@@ -131,7 +131,7 @@
-
+
@@ -140,7 +140,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml
index 41d3ca3020c98..2283a0e4d6158 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml
@@ -148,7 +148,7 @@
-
+
@@ -205,7 +205,7 @@
-
+
@@ -264,7 +264,7 @@
stepKey="changeVisibility"/>
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml
index bae81513de632..df09768139533 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml
@@ -61,20 +61,20 @@
-
+
-
+
-
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml
new file mode 100644
index 0000000000000..6f65865924bad
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml
index 2884cb26cf813..df2b525a56086 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml
@@ -45,7 +45,7 @@
-
+
@@ -57,4 +57,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml
index 8149bc34087fb..455e77666c3a2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductImageAssignmentForMultipleStoresTest.xml
@@ -50,7 +50,7 @@
-
+
@@ -73,8 +73,8 @@
-
-
+
+
@@ -82,19 +82,19 @@
-
+
-
+
-
+
@@ -105,7 +105,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
index de065d2d930cb..0b7e2a70735c3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
@@ -42,14 +42,14 @@
-
+
-
+
-
+
@@ -80,11 +80,11 @@
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml
index b5f212d1144be..fb54b0b601d85 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml
@@ -27,66 +27,66 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+