Skip to content

Commit f953c3d

Browse files
committed
MC-29188: Storefront: Create configurable product on (multiple websites/multiple storeviews)
1 parent f956769 commit f953c3d

7 files changed

+700
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Block\Product\View\Type;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
13+
use Magento\Catalog\Model\ResourceModel\Product as ProductResource;
14+
use Magento\Framework\ObjectManagerInterface;
15+
use Magento\Framework\Serialize\SerializerInterface;
16+
use Magento\Framework\View\LayoutInterface;
17+
use Magento\Store\Model\StoreManagerInterface;
18+
use Magento\TestFramework\Helper\Bootstrap;
19+
use PHPUnit\Framework\TestCase;
20+
21+
/**
22+
* Class check configurable product options displaying per stores
23+
*
24+
* @magentoDbIsolation disabled
25+
*/
26+
class MultiStoreConfigurableViewOnProductPageTest extends TestCase
27+
{
28+
/** @var ObjectManagerInterface */
29+
private $objectManager;
30+
31+
/** @var ProductRepositoryInterface */
32+
private $productRepository;
33+
34+
/** @var StoreManagerInterface */
35+
private $storeManager;
36+
37+
/** @var Configurable */
38+
private $block;
39+
40+
/** @var LayoutInterface */
41+
private $layout;
42+
43+
/** @var SerializerInterface */
44+
private $serializer;
45+
46+
/** @var ProductResource */
47+
private $productResource;
48+
49+
/**
50+
* @inheritdoc
51+
*/
52+
protected function setUp()
53+
{
54+
parent::setUp();
55+
56+
$this->objectManager = Bootstrap::getObjectManager();
57+
$this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
58+
$this->storeManager = $this->objectManager->get(StoreManagerInterface::class);
59+
$this->layout = $this->objectManager->get(LayoutInterface::class);
60+
$this->block = $this->layout->createBlock(Configurable::class);
61+
$this->serializer = $this->objectManager->get(SerializerInterface::class);
62+
$this->productResource = $this->objectManager->get(ProductResource::class);
63+
}
64+
65+
/**
66+
* @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_different_option_labeles_per_stores.php
67+
*
68+
* @dataProvider expectedLabelsDataProvider
69+
*
70+
* @param array $expectedStoreData
71+
* @param array $expectedSecondSoreData
72+
* @return void
73+
*/
74+
public function testMultiStoreLabelView(array $expectedStoreData, array $expectedSecondSoreData): void
75+
{
76+
$this->assertProductLabelConfigDataPerStore($expectedStoreData);
77+
$this->assertProductLabelConfigDataPerStore($expectedSecondSoreData, 'fixturestore', true);
78+
}
79+
80+
/**
81+
* @return array
82+
*/
83+
public function expectedLabelsDataProvider(): array
84+
{
85+
return [
86+
[
87+
'options_first_store' => [
88+
[
89+
'label' => 'Option 1 Default Store',
90+
'sku' => 'simple_option_1_default_store',
91+
],
92+
[
93+
'label' => 'Option 2 Default Store',
94+
'sku' => 'simple_option_2_default_store',
95+
],
96+
[
97+
'label' => 'Option 3 Default Store',
98+
'sku' => 'simple_option_3_default_store',
99+
],
100+
],
101+
'options_second_store' => [
102+
[
103+
'label' => 'Option 1 Second Store',
104+
'sku' => 'simple_option_1_default_store',
105+
],
106+
[
107+
'label' => 'Option 2 Second Store',
108+
'sku' => 'simple_option_2_default_store',
109+
],
110+
[
111+
'label' => 'Option 3 Second Store',
112+
'sku' => 'simple_option_3_default_store',
113+
],
114+
],
115+
],
116+
];
117+
}
118+
119+
/**
120+
* @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_two_websites.php
121+
*
122+
* @dataProvider expectedProductDataProvider
123+
*
124+
* @param array $expectedProducts
125+
* @param array $expectedSecondStoreProducts
126+
* @return void
127+
*/
128+
public function testMultiStoreOptionsView(array $expectedProducts, array $expectedSecondStoreProducts): void
129+
{
130+
$this->prepareConfigurableProduct('configurable', 'fixture_second_store');
131+
$this->assertProductConfigPerStore($expectedProducts);
132+
$this->assertProductConfigPerStore($expectedSecondStoreProducts, 'fixture_second_store', true);
133+
}
134+
135+
/**
136+
* @return array
137+
*/
138+
public function expectedProductDataProvider(): array
139+
{
140+
return [
141+
[
142+
'expected_store_products' => ['simple_option_1', 'simple_option_2'],
143+
'expected_second_store_products' => ['simple_option_2'],
144+
],
145+
];
146+
}
147+
148+
/**
149+
* Prepare configurable product to test
150+
*
151+
* @param string $sku
152+
* @param string $storeCode
153+
* @return void
154+
*/
155+
private function prepareConfigurableProduct(string $sku, string $storeCode): void
156+
{
157+
$product = $this->productRepository->get($sku, false, null, true);
158+
$productToUpdate = $product->getTypeInstance()->getUsedProductCollection($product)->getFirstItem();
159+
$this->assertNotEmpty($productToUpdate->getData(), 'Configurable product does not have a child');
160+
$this->setProductDisabledPerStore($productToUpdate, $storeCode);
161+
}
162+
163+
/**
164+
* Assert product options display per stores
165+
*
166+
* @param array $expectedProducts
167+
* @param array $config
168+
* @return void
169+
*/
170+
private function assertProductConfig(array $expectedProducts, array $config): void
171+
{
172+
$this->assertCount(count($expectedProducts), $config);
173+
$idsBySkus = $this->productResource->getProductsIdsBySkus($expectedProducts);
174+
175+
foreach ($idsBySkus as $productId) {
176+
$this->assertArrayHasKey($productId, $config);
177+
}
178+
}
179+
180+
/**
181+
* Set product status attribute to disabled
182+
*
183+
* @param ProductInterface $product
184+
* @param string $storeCode
185+
* @return void
186+
*/
187+
private function setProductDisabledPerStore(ProductInterface $product, string $storeCode)
188+
{
189+
$currentStore = $this->storeManager->getStore();
190+
try {
191+
$this->storeManager->setCurrentStore($storeCode);
192+
$product->setStatus(Status::STATUS_DISABLED);
193+
$this->productRepository->save($product);
194+
} finally {
195+
$this->storeManager->setCurrentStore($currentStore);
196+
}
197+
}
198+
199+
/**
200+
* Assert configurable product config per stores
201+
*
202+
* @param array $expectedProducts
203+
* @param string $storeCode
204+
* @param bool $refreshBlock
205+
* @return void
206+
*/
207+
private function assertProductConfigPerStore(
208+
array $expectedProducts,
209+
string $storeCode = 'default',
210+
bool $refreshBlock = false
211+
): void {
212+
$currentStore = $this->storeManager->getStore();
213+
try {
214+
if ($currentStore->getCode() !== $storeCode) {
215+
$this->storeManager->setCurrentStore($storeCode);
216+
}
217+
$product = $this->productRepository->get('configurable', false, null, true);
218+
$config = $this->getBlockConfig($product, $refreshBlock)['index'] ?? null;
219+
$this->assertNotNull($config);
220+
$this->assertProductConfig($expectedProducts, $config);
221+
} finally {
222+
if ($currentStore->getCode() !== $storeCode) {
223+
$this->storeManager->setCurrentStore($currentStore);
224+
}
225+
}
226+
}
227+
228+
/**
229+
* Assert configurable product labels config per stores
230+
*
231+
* @param array $expectedStoreData
232+
* @param string $storeCode
233+
* @param bool $refreshBlock
234+
* @return void
235+
*/
236+
private function assertProductLabelConfigDataPerStore(
237+
array $expectedStoreData,
238+
string $storeCode = 'default',
239+
bool $refreshBlock = false
240+
): void {
241+
$currentStore = $this->storeManager->getStore();
242+
try {
243+
if ($currentStore->getCode() !== $storeCode) {
244+
$this->storeManager->setCurrentStore($storeCode);
245+
}
246+
$product = $this->productRepository->get('configurable', false, null, true);
247+
$config = $this->getBlockConfig($product, $refreshBlock)['attributes'] ?? null;
248+
$this->assertNotNull($config);
249+
$this->assertAttributeConfig($expectedStoreData, reset($config));
250+
} finally {
251+
if ($currentStore->getCode() !== $storeCode) {
252+
$this->storeManager->setCurrentStore($currentStore);
253+
}
254+
}
255+
}
256+
257+
/**
258+
* Get view block
259+
*
260+
* @param bool $refresh
261+
* @return Configurable
262+
*/
263+
private function getBlock(bool $refresh = false): Configurable
264+
{
265+
if ($refresh) {
266+
$this->block = $this->layout->createBlock(Configurable::class);
267+
}
268+
return $this->block;
269+
}
270+
271+
/**
272+
* Get block config
273+
*
274+
* @param ProductInterface $product
275+
* @param bool $refreshBlock
276+
* @return array
277+
*/
278+
private function getBlockConfig(ProductInterface $product, bool $refreshBlock): array
279+
{
280+
$this->getBlock($refreshBlock)->setProduct($product);
281+
282+
return $this->serializer->unserialize($this->getBlock()->getJsonConfig());
283+
}
284+
285+
/**
286+
* Assert configurable product config
287+
*
288+
* @param array $expectedData
289+
* @param array $actualOptions
290+
* @return void
291+
*/
292+
private function assertAttributeConfig(array $expectedData, array $actualOptions): void
293+
{
294+
$skus = array_column($expectedData, 'sku');
295+
$idBySkuMap = $this->productResource->getProductsIdsBySkus($skus);
296+
foreach ($expectedData as &$option) {
297+
$sku = $option['sku'];
298+
unset($option['sku']);
299+
$option['products'] = [$idBySkuMap[$sku]];
300+
$found = false;
301+
foreach ($actualOptions['options'] as $actualOption) {
302+
if ($option['label'] === $actualOption['label']) {
303+
unset($actualOption['id']);
304+
$this->assertEquals($option, $actualOption);
305+
$found = true;
306+
break;
307+
}
308+
}
309+
$this->assertTrue($found, sprintf('The option with %s label is not loaded', $option['label']));
310+
}
311+
}
312+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Catalog\Api\Data\ProductAttributeInterface;
9+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
10+
use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory;
11+
use Magento\Catalog\Setup\CategorySetup;
12+
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
13+
use Magento\Store\Model\Store;
14+
use Magento\Store\Model\StoreManagerInterface;
15+
use Magento\TestFramework\Helper\Bootstrap;
16+
17+
require __DIR__ . '/../../Store/_files/core_fixturestore.php';
18+
19+
$objectManager = Bootstrap::getObjectManager();
20+
$storeManager = $objectManager->get(StoreManagerInterface::class);
21+
$defaultInstalledStoreId = $storeManager->getStore('default')->getId();
22+
$secondStoreId = $storeManager->getStore('fixturestore')->getId();
23+
$installer = $objectManager->create(CategorySetup::class);
24+
$attribute = $objectManager->create(AttributeFactory::class)->create();
25+
$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class);
26+
$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE);
27+
if (!$attribute->loadByCode($entityType, 'different_labels_dropdown_attribute')->getAttributeId()) {
28+
$attribute->setData(
29+
[
30+
'frontend_label' => ['Different option labels dropdown attribute'],
31+
'entity_type_id' => $entityType,
32+
'frontend_input' => 'select',
33+
'backend_type' => 'int',
34+
'is_required' => '0',
35+
'attribute_code' => 'different_labels_dropdown_attribute',
36+
'is_global' => ScopedAttributeInterface::SCOPE_GLOBAL,
37+
'is_user_defined' => 1,
38+
'is_unique' => '0',
39+
'is_searchable' => '0',
40+
'is_comparable' => '0',
41+
'is_filterable' => '1',
42+
'is_filterable_in_search' => '0',
43+
'is_used_for_promo_rules' => '0',
44+
'is_html_allowed_on_front' => '1',
45+
'used_in_product_listing' => '1',
46+
'used_for_sort_by' => '0',
47+
'option' => [
48+
'value' => [
49+
'option_1' => [
50+
Store::DEFAULT_STORE_ID => 'Option 1',
51+
$defaultInstalledStoreId => 'Option 1 Default Store',
52+
$secondStoreId => 'Option 1 Second Store',
53+
],
54+
'option_2' => [
55+
Store::DEFAULT_STORE_ID => 'Option 2',
56+
$defaultInstalledStoreId => 'Option 2 Default Store',
57+
$secondStoreId => 'Option 2 Second Store',
58+
],
59+
'option_3' => [
60+
Store::DEFAULT_STORE_ID => 'Option 3',
61+
$defaultInstalledStoreId => 'Option 3 Default Store',
62+
$secondStoreId => 'Option 3 Second Store',
63+
],
64+
],
65+
'order' => [
66+
'option_1' => 1,
67+
'option_2' => 2,
68+
'option_3' => 3,
69+
],
70+
],
71+
]
72+
);
73+
$attribute->save();
74+
$installer->addAttributeToGroup(
75+
ProductAttributeInterface::ENTITY_TYPE_CODE,
76+
'Default',
77+
'General',
78+
$attribute->getId()
79+
);
80+
}

0 commit comments

Comments
 (0)