Skip to content

Commit 1a52c07

Browse files
MC-25146: Layered Navigation with different product attributes on Catalog Search Results page
1 parent ad6c093 commit 1a52c07

16 files changed

+983
-301
lines changed

dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractCategoryTest.php

Lines changed: 0 additions & 101 deletions
This file was deleted.
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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\LayeredNavigation\Block\Navigation;
9+
10+
use Magento\Catalog\Api\Data\CategoryInterface;
11+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
12+
use Magento\Catalog\Api\ProductRepositoryInterface;
13+
use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
14+
use Magento\Catalog\Model\Layer\Filter\Item;
15+
use Magento\Catalog\Model\Layer\Resolver;
16+
use Magento\Catalog\Model\ResourceModel\Category\Collection;
17+
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
18+
use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor;
19+
use Magento\Framework\ObjectManagerInterface;
20+
use Magento\Framework\Search\Request\Builder;
21+
use Magento\Framework\Search\Request\Config;
22+
use Magento\Framework\View\LayoutInterface;
23+
use Magento\LayeredNavigation\Block\Navigation;
24+
use Magento\LayeredNavigation\Block\Navigation\Search as SearchNavigationBlock;
25+
use Magento\LayeredNavigation\Block\Navigation\Category as CategoryNavigationBlock;
26+
use Magento\Search\Model\Search;
27+
use Magento\Store\Model\Store;
28+
use Magento\TestFramework\Helper\Bootstrap;
29+
use PHPUnit\Framework\TestCase;
30+
31+
/**
32+
* Base class for custom filters in navigation block on category page.
33+
*
34+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
35+
*/
36+
abstract class AbstractFiltersTest extends TestCase
37+
{
38+
/**
39+
* @var ObjectManagerInterface
40+
*/
41+
protected $objectManager;
42+
43+
/**
44+
* @var CollectionFactory
45+
*/
46+
protected $categoryCollectionFactory;
47+
48+
/**
49+
* @var Navigation
50+
*/
51+
protected $navigationBlock;
52+
53+
/**
54+
* @var LayoutInterface
55+
*/
56+
protected $layout;
57+
58+
/**
59+
* @var ProductAttributeRepositoryInterface
60+
*/
61+
protected $attributeRepository;
62+
63+
/**
64+
* @var ProductRepositoryInterface
65+
*/
66+
protected $productRepository;
67+
68+
/**
69+
* @inheritdoc
70+
*/
71+
protected function setUp()
72+
{
73+
parent::setUp();
74+
$this->objectManager = Bootstrap::getObjectManager();
75+
$this->categoryCollectionFactory = $this->objectManager->create(CollectionFactory::class);
76+
$this->layout = $this->objectManager->get(LayoutInterface::class);
77+
$layerResolver = $this->objectManager->create(Resolver::class);
78+
79+
if ($this->getLayerType() === Resolver::CATALOG_LAYER_SEARCH) {
80+
$layerResolver->create(Resolver::CATALOG_LAYER_SEARCH);
81+
$this->navigationBlock = $this->objectManager->create(
82+
SearchNavigationBlock::class,
83+
[
84+
'layerResolver' => $layerResolver,
85+
]
86+
);
87+
} else {
88+
$this->navigationBlock = $this->objectManager->create(CategoryNavigationBlock::class);
89+
}
90+
91+
$this->attributeRepository = $this->objectManager->create(ProductAttributeRepositoryInterface::class);
92+
$this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
93+
}
94+
95+
/**
96+
* Returns layer type for navigation block.
97+
*
98+
* @return string
99+
*/
100+
abstract protected function getLayerType(): string;
101+
102+
/**
103+
* Returns attribute code.
104+
*
105+
* @return string
106+
*/
107+
abstract protected function getAttributeCode(): string;
108+
109+
/**
110+
* Tests getFilters method from navigation block on category page.
111+
*
112+
* @param array $products
113+
* @param array $attributeData
114+
* @param array $expectation
115+
* @param string $categoryName
116+
* @return void
117+
*/
118+
protected function getCategoryFiltersAndAssert(
119+
array $products,
120+
array $attributeData,
121+
array $expectation,
122+
string $categoryName
123+
): void {
124+
$this->updateAttribute($this->getAttributeCode(), $attributeData);
125+
$this->updateProducts($products, $this->getAttributeCode());
126+
$this->clearInstanceAndReindexSearch();
127+
$category = $this->loadCategory($categoryName, Store::DEFAULT_STORE_ID);
128+
$this->navigationBlock->getLayer()->setCurrentCategory($category);
129+
$this->navigationBlock->setLayout($this->layout);
130+
$filter = $this->getFilterByCode($this->navigationBlock->getFilters(), $this->getAttributeCode());
131+
132+
if ($attributeData['is_filterable']) {
133+
$this->assertNotNull($filter);
134+
$this->assertEquals($expectation, $this->prepareFilterItems($filter));
135+
} else {
136+
$this->assertNull($filter);
137+
}
138+
}
139+
140+
/**
141+
* Tests getFilters method from navigation block on search page.
142+
*
143+
* @param array $products
144+
* @param array $attributeData
145+
* @param array $expectation
146+
* @return void
147+
*/
148+
protected function getSearchFiltersAndAssert(
149+
array $products,
150+
array $attributeData,
151+
array $expectation
152+
): void {
153+
$this->updateAttribute($this->getAttributeCode(), $attributeData);
154+
$this->updateProducts($products, $this->getAttributeCode());
155+
$this->clearInstanceAndReindexSearch();
156+
$this->navigationBlock->getRequest()->setParams(['q' => 'Simple Product']);
157+
$this->navigationBlock->setLayout($this->layout);
158+
$filter = $this->getFilterByCode($this->navigationBlock->getFilters(), $this->getAttributeCode());
159+
160+
if ($attributeData['is_filterable_in_search']) {
161+
$this->assertNotNull($filter);
162+
$this->assertEquals($expectation, $this->prepareFilterItems($filter));
163+
} else {
164+
$this->assertNull($filter);
165+
}
166+
}
167+
168+
/**
169+
* Returns filter with specified attribute.
170+
*
171+
* @param AbstractFilter[] $filters
172+
* @param string $code
173+
* @return AbstractFilter|null
174+
*/
175+
protected function getFilterByCode(array $filters, string $code): ?AbstractFilter
176+
{
177+
$filter = array_filter(
178+
$filters,
179+
function (AbstractFilter $filter) use ($code) {
180+
return $filter->getData('attribute_model')
181+
&& $filter->getData('attribute_model')->getAttributeCode() === $code;
182+
}
183+
);
184+
185+
return array_shift($filter);
186+
}
187+
188+
/**
189+
* Updates attribute data.
190+
*
191+
* @param string $attributeCode
192+
* @param array $data
193+
* @return void
194+
*/
195+
protected function updateAttribute(
196+
string $attributeCode,
197+
array $data
198+
): void {
199+
$attribute = $this->attributeRepository->get($attributeCode);
200+
$attribute->addData($data);
201+
$this->attributeRepository->save($attribute);
202+
}
203+
204+
/**
205+
* Returns filter items as array.
206+
*
207+
* @param AbstractFilter $filter
208+
* @return array
209+
*/
210+
protected function prepareFilterItems(AbstractFilter $filter): array
211+
{
212+
$items = [];
213+
/** @var Item $item */
214+
foreach ($filter->getItems() as $item) {
215+
$items[] = [
216+
'label' => $item->getData('label'),
217+
'count' => $item->getData('count'),
218+
];
219+
}
220+
221+
return $items;
222+
}
223+
224+
/**
225+
* Update products data by attribute.
226+
*
227+
* @param array $products
228+
* @param string $attributeCode
229+
* @return void
230+
*/
231+
protected function updateProducts(array $products, string $attributeCode): void
232+
{
233+
$attribute = $this->attributeRepository->get($attributeCode);
234+
235+
foreach ($products as $productSku => $stringValue) {
236+
$product = $this->productRepository->get($productSku, false, Store::DEFAULT_STORE_ID, true);
237+
$product->addData(
238+
[$attribute->getAttributeCode() => $attribute->getSource()->getOptionId($stringValue)]
239+
);
240+
$this->productRepository->save($product);
241+
}
242+
}
243+
244+
/**
245+
* Clears instances and rebuilds seqrch index.
246+
*
247+
* @return void
248+
*/
249+
protected function clearInstanceAndReindexSearch(): void
250+
{
251+
$this->objectManager->removeSharedInstance(Config::class);
252+
$this->objectManager->removeSharedInstance(Builder::class);
253+
$this->objectManager->removeSharedInstance(Search::class);
254+
$this->objectManager->create(Processor::class)->reindexAll();
255+
}
256+
257+
/**
258+
* Loads category by id.
259+
*
260+
* @param string $categoryName
261+
* @param int $storeId
262+
* @return CategoryInterface
263+
*/
264+
protected function loadCategory(string $categoryName, int $storeId): CategoryInterface
265+
{
266+
/** @var Collection $categoryCollection */
267+
$categoryCollection = $this->categoryCollectionFactory->create();
268+
/** @var CategoryInterface $category */
269+
$category = $categoryCollection->setStoreId($storeId)
270+
->addAttributeToSelect('display_mode', 'left')
271+
->addAttributeToFilter(CategoryInterface::KEY_NAME, $categoryName)
272+
->setPageSize(1)
273+
->getFirstItem();
274+
$category->setStoreId($storeId);
275+
276+
return $category;
277+
}
278+
}

0 commit comments

Comments
 (0)