diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml
new file mode 100644
index 0000000000000..8d66427c5392e
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
index 1555e88700a45..2f333e7ca6f6e 100644
--- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
+++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php
@@ -4,11 +4,21 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel;
+use Magento\Catalog\Api\Data\ProductAttributeInterface;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
+use Magento\ConfigurableProduct\Api\Data\OptionInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
+use Magento\Framework\Api\FilterBuilder;
+use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Indexer\ActionInterface;
+/**
+ * Plugin product resource model
+ */
class Product
{
/**
@@ -21,18 +31,45 @@ class Product
*/
private $productIndexer;
+ /**
+ * @var ProductAttributeRepositoryInterface
+ */
+ private $productAttributeRepository;
+
+ /**
+ * @var SearchCriteriaBuilder
+ */
+ private $searchCriteriaBuilder;
+
+ /**
+ * @var FilterBuilder
+ */
+ private $filterBuilder;
+
/**
* Initialize Product dependencies.
*
* @param Configurable $configurable
* @param ActionInterface $productIndexer
+ * @param ProductAttributeRepositoryInterface $productAttributeRepository
+ * @param SearchCriteriaBuilder $searchCriteriaBuilder
+ * @param FilterBuilder $filterBuilder
*/
public function __construct(
Configurable $configurable,
- ActionInterface $productIndexer
+ ActionInterface $productIndexer,
+ ProductAttributeRepositoryInterface $productAttributeRepository = null,
+ SearchCriteriaBuilder $searchCriteriaBuilder = null,
+ FilterBuilder $filterBuilder = null
) {
$this->configurable = $configurable;
$this->productIndexer = $productIndexer;
+ $this->productAttributeRepository = $productAttributeRepository ?: ObjectManager::getInstance()
+ ->get(ProductAttributeRepositoryInterface::class);
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance()
+ ->get(SearchCriteriaBuilder::class);
+ $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance()
+ ->get(FilterBuilder::class);
}
/**
@@ -41,6 +78,7 @@ public function __construct(
* @param \Magento\Catalog\Model\ResourceModel\Product $subject
* @param \Magento\Framework\DataObject $object
* @return void
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -51,6 +89,39 @@ public function beforeSave(
/** @var \Magento\Catalog\Model\Product $object */
if ($object->getTypeId() == Configurable::TYPE_CODE) {
$object->getTypeInstance()->getSetAttributes($object);
+ $this->resetConfigurableOptionsData($object);
+ }
+ }
+
+ /**
+ * Set null for configurable options attribute of configurable product
+ *
+ * @param \Magento\Catalog\Model\Product $object
+ * @return void
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function resetConfigurableOptionsData($object)
+ {
+ $extensionAttribute = $object->getExtensionAttributes();
+ if ($extensionAttribute && $extensionAttribute->getConfigurableProductOptions()) {
+ $attributeIds = [];
+ /** @var OptionInterface $option */
+ foreach ($extensionAttribute->getConfigurableProductOptions() as $option) {
+ $attributeIds[] = $option->getAttributeId();
+ }
+
+ $filter = $this->filterBuilder
+ ->setField(ProductAttributeInterface::ATTRIBUTE_ID)
+ ->setConditionType('in')
+ ->setValue($attributeIds)
+ ->create();
+ $this->searchCriteriaBuilder->addFilters([$filter]);
+ $searchCriteria = $this->searchCriteriaBuilder->create();
+ $optionAttributes = $this->productAttributeRepository->getList($searchCriteria)->getItems();
+
+ foreach ($optionAttributes as $optionAttribute) {
+ $object->setData($optionAttribute->getAttributeCode(), null);
+ }
}
}
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml
new file mode 100644
index 0000000000000..c48f22a3656d5
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ Adds 3 provided Options to a new Attribute on the Configurable Product creation/edit page. Selected default first option. Set "Use in Layered Navigation" to "Yes".
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml
new file mode 100644
index 0000000000000..cc709b80efebb
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Goes to the Admin Product grid page. Fill basic value for Configurable Product using the default Product Options.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml
new file mode 100644
index 0000000000000..969a41e27d459
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Goes to the select values page from each attribute to include in the product.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml
new file mode 100644
index 0000000000000..cc2ff9a63ae40
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Click to check option.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml
new file mode 100644
index 0000000000000..3cca319d9569c
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Set quantity 1 to all child skus for configurable product. Save a configurable product and confirm.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
index abab103fa6d37..3d5a0d1cc6a3f 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php
@@ -7,20 +7,38 @@
namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Model\ResourceModel;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
+use Magento\Catalog\Model\Product as ModelProduct;
use Magento\Catalog\Model\Product\Type;
+use Magento\Catalog\Model\ProductAttributeSearchResults;
+use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute;
+use Magento\Catalog\Model\ResourceModel\Product as ResourceModelProduct;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
-use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute;
+use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product as PluginResourceModelProduct;
+use Magento\Framework\Api\ExtensionAttributesInterface;
+use Magento\Framework\Api\FilterBuilder;
+use Magento\Framework\Api\SearchCriteria;
+use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Indexer\ActionInterface;
-use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+/**
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class ProductTest extends TestCase
{
/**
- * @var ObjectManager
+ * @var PluginResourceModelProduct
*/
- private $objectManager;
+ private $model;
+
+ /**
+ * @var ObjectManagerHelper
+ */
+ private $objectManagerHelper;
/**
* @var Configurable|MockObject
@@ -33,39 +51,128 @@ class ProductTest extends TestCase
private $actionMock;
/**
- * @var Product
+ * @var ProductAttributeRepositoryInterface|MockObject
*/
- private $model;
+ private $productAttributeRepositoryMock;
+
+ /**
+ * @var SearchCriteriaBuilder|MockObject
+ */
+ private $searchCriteriaBuilderMock;
+
+ /**
+ * @var FilterBuilder|MockObject
+ */
+ private $filterBuilderMock;
protected function setUp(): void
{
- $this->objectManager = new ObjectManager($this);
$this->configurableMock = $this->createMock(Configurable::class);
$this->actionMock = $this->getMockForAbstractClass(ActionInterface::class);
-
- $this->model = $this->objectManager->getObject(
- Product::class,
+ $this->productAttributeRepositoryMock = $this->getMockBuilder(ProductAttributeRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getList'])
+ ->getMockForAbstractClass();
+ $this->searchCriteriaBuilderMock = $this->createPartialMock(
+ SearchCriteriaBuilder::class,
+ ['addFilters', 'create']
+ );
+ $this->filterBuilderMock = $this->createPartialMock(
+ FilterBuilder::class,
+ ['setField', 'setConditionType', 'setValue', 'create']
+ );
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->model = $this->objectManagerHelper->getObject(
+ PluginResourceModelProduct::class,
[
'configurable' => $this->configurableMock,
'productIndexer' => $this->actionMock,
+ 'productAttributeRepository' => $this->productAttributeRepositoryMock,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
+ 'filterBuilder' => $this->filterBuilderMock
]
);
}
- public function testBeforeSaveConfigurable()
+ public function testBeforeSaveConfigurable(): void
{
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $object */
- $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']);
+ /** @var ResourceModelProduct|MockObject $subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $object */
+ $object = $this->createPartialMock(
+ ModelProduct::class,
+ [
+ 'getTypeId',
+ 'getTypeInstance',
+ 'getExtensionAttributes',
+ 'setData'
+ ]
+ );
$type = $this->createPartialMock(
Configurable::class,
['getSetAttributes']
);
- $type->expects($this->once())->method('getSetAttributes')->with($object);
-
- $object->expects($this->once())->method('getTypeId')->willReturn(Configurable::TYPE_CODE);
- $object->expects($this->once())->method('getTypeInstance')->willReturn($type);
+ $extensionAttributes = $this->getMockBuilder(ExtensionAttributesInterface::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getConfigurableProductOptions'])
+ ->getMock();
+ $option = $this->createPartialMock(
+ ConfigurableAttribute::class,
+ ['getAttributeId']
+ );
+ $extensionAttributes->expects($this->exactly(2))
+ ->method('getConfigurableProductOptions')
+ ->willReturn([$option]);
+ $object->expects($this->once())
+ ->method('getExtensionAttributes')
+ ->willReturn($extensionAttributes);
+
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setField')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setValue')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('setConditionType')
+ ->willReturnSelf();
+ $this->filterBuilderMock->expects($this->atLeastOnce())
+ ->method('create')
+ ->willReturnSelf();
+ $searchCriteria = $this->createMock(SearchCriteria::class);
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('create')
+ ->willReturn($searchCriteria);
+ $searchResultMockClass = $this->createPartialMock(
+ ProductAttributeSearchResults::class,
+ ['getItems']
+ );
+ $this->productAttributeRepositoryMock->expects($this->once())
+ ->method('getList')
+ ->with($searchCriteria)
+ ->willReturn($searchResultMockClass);
+ $optionAttribute = $this->createPartialMock(
+ EavAttribute::class,
+ ['getAttributeCode']
+ );
+ $searchResultMockClass->expects($this->once())
+ ->method('getItems')
+ ->willReturn([$optionAttribute]);
+ $type->expects($this->once())
+ ->method('getSetAttributes')
+ ->with($object);
+ $object->expects($this->once())
+ ->method('getTypeId')
+ ->will($this->returnValue(Configurable::TYPE_CODE));
+ $object->expects($this->once())
+ ->method('getTypeInstance')
+ ->will($this->returnValue($type));
+ $object->expects($this->once())
+ ->method('setData');
+ $option->expects($this->once())
+ ->method('getAttributeId');
+ $optionAttribute->expects($this->once())
+ ->method('getAttributeCode');
$this->model->beforeSave(
$subject,
@@ -73,14 +180,23 @@ public function testBeforeSaveConfigurable()
);
}
- public function testBeforeSaveSimple()
+ public function testBeforeSaveSimple(): void
{
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $object */
- $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']);
- $object->expects($this->once())->method('getTypeId')->willReturn(Type::TYPE_SIMPLE);
- $object->expects($this->never())->method('getTypeInstance');
+ /** @var ResourceModelProduct|MockObject$subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $object */
+ $object = $this->createPartialMock(
+ ModelProduct::class,
+ [
+ 'getTypeId',
+ 'getTypeInstance'
+ ]
+ );
+ $object->expects($this->once())
+ ->method('getTypeId')
+ ->will($this->returnValue(Type::TYPE_SIMPLE));
+ $object->expects($this->never())
+ ->method('getTypeInstance');
$this->model->beforeSave(
$subject,
@@ -88,29 +204,35 @@ public function testBeforeSaveSimple()
);
}
- public function testAroundDelete()
+ public function testAroundDelete(): void
{
$productId = '1';
$parentConfigId = ['2'];
- /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */
- $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class);
- /** @var \Magento\Catalog\Model\Product|MockObject $product */
+ /** @var ResourceModelProduct|MockObject $subject */
+ $subject = $this->createMock(ResourceModelProduct::class);
+ /** @var ModelProduct|MockObject $product */
$product = $this->createPartialMock(
- \Magento\Catalog\Model\Product::class,
+ ModelProduct::class,
['getId', 'delete']
);
- $product->expects($this->once())->method('getId')->willReturn($productId);
- $product->expects($this->once())->method('delete')->willReturn(true);
+ $product->expects($this->once())
+ ->method('getId')
+ ->willReturn($productId);
+ $product->expects($this->once())
+ ->method('delete')
+ ->willReturn(true);
$this->configurableMock->expects($this->once())
->method('getParentIdsByChild')
->with($productId)
->willReturn($parentConfigId);
- $this->actionMock->expects($this->once())->method('executeList')->with($parentConfigId);
+ $this->actionMock->expects($this->once())
+ ->method('executeList')
+ ->with($parentConfigId);
$return = $this->model->aroundDelete(
$subject,
- /** @var \Magento\Catalog\Model\Product|MockObject $prod */
- function (\Magento\Catalog\Model\Product $prod) use ($subject) {
+ /** @var ModelProduct|MockObject $prod */
+ function (ModelProduct $prod) use ($subject) {
$prod->delete();
return $subject;
},