Skip to content

Commit b2a3277

Browse files
MC-25192: Sorting products in category page
1 parent 9400c1e commit b2a3277

File tree

3 files changed

+378
-0
lines changed

3 files changed

+378
-0
lines changed
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
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\Catalog\Block\Product\ListProduct;
9+
10+
use Magento\Catalog\Api\CategoryRepositoryInterface;
11+
use Magento\Catalog\Api\Data\CategoryInterface;
12+
use Magento\Catalog\Block\Product\ListProduct;
13+
use Magento\Catalog\Block\Product\ProductList\Toolbar;
14+
use Magento\Catalog\Model\Config;
15+
use Magento\Catalog\Model\ResourceModel\Category\Collection;
16+
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
17+
use Magento\Framework\App\Config\MutableScopeConfigInterface;
18+
use Magento\Framework\ObjectManagerInterface;
19+
use Magento\Framework\View\LayoutInterface;
20+
use Magento\Store\Model\ScopeInterface;
21+
use Magento\Store\Model\Store;
22+
use Magento\Store\Model\StoreManagerInterface;
23+
use Magento\TestFramework\Helper\Bootstrap;
24+
use PHPUnit\Framework\TestCase;
25+
26+
/**
27+
* Tests for products sorting on category page.
28+
*
29+
* @magentoDbIsolation disabled
30+
* @magentoAppIsolation enabled
31+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
32+
*/
33+
class SortingTest extends TestCase
34+
{
35+
/**
36+
* @var ObjectManagerInterface
37+
*/
38+
private $objectManager;
39+
40+
/**
41+
* @var ListProduct
42+
*/
43+
private $block;
44+
45+
/**
46+
* @var CollectionFactory
47+
*/
48+
private $categoryCollectionFactory;
49+
50+
/**
51+
* @var CategoryRepositoryInterface
52+
*/
53+
private $categoryRepository;
54+
55+
/**
56+
* @var StoreManagerInterface
57+
*/
58+
private $storeManager;
59+
60+
/**
61+
* @var LayoutInterface
62+
*/
63+
private $layout;
64+
65+
/**
66+
* @var MutableScopeConfigInterface
67+
*/
68+
private $scopeConfig;
69+
70+
/**
71+
* @inheritdoc
72+
*/
73+
protected function setUp()
74+
{
75+
$this->objectManager = Bootstrap::getObjectManager();
76+
$this->storeManager = $this->objectManager->get(StoreManagerInterface::class);
77+
$this->layout = $this->objectManager->get(LayoutInterface::class);
78+
$this->layout->createBlock(Toolbar::class, 'product_list_toolbar');
79+
$this->block = $this->layout->createBlock(ListProduct::class)->setToolbarBlockName('product_list_toolbar');
80+
$this->categoryCollectionFactory = $this->objectManager->get(CollectionFactory::class);
81+
$this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class);
82+
$this->scopeConfig = $this->objectManager->get(MutableScopeConfigInterface::class);
83+
parent::setUp();
84+
}
85+
86+
/**
87+
* @magentoDataFixture Magento/Catalog/_files/products_with_not_empty_layered_navigation_attribute.php
88+
* @dataProvider productListSortOrderDataProvider
89+
* @param string $sortBy
90+
* @param string $direction
91+
* @param array $expectation
92+
* @return void
93+
*/
94+
public function testProductListSortOrder(string $sortBy, string $direction, array $expectation): void
95+
{
96+
$category = $this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, $sortBy);
97+
$this->renderBlock($category, $direction);
98+
$this->assertBlockSorting($sortBy, $expectation);
99+
}
100+
101+
/**
102+
* @magentoDataFixture Magento/Catalog/_files/products_with_not_empty_layered_navigation_attribute.php
103+
* @dataProvider productListSortOrderDataProvider
104+
* @param string $sortBy
105+
* @param string $direction
106+
* @param array $expectation
107+
* @return void
108+
*/
109+
public function testProductListSortOrderWithConfig(string $sortBy, string $direction, array $expectation): void
110+
{
111+
$this->objectManager->removeSharedInstance(Config::class);
112+
$this->scopeConfig->setValue(
113+
Config::XML_PATH_LIST_DEFAULT_SORT_BY,
114+
$sortBy,
115+
ScopeInterface::SCOPE_STORE,
116+
Store::DEFAULT_STORE_ID
117+
);
118+
$category = $this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, null);
119+
$this->renderBlock($category, $direction);
120+
$this->assertBlockSorting($sortBy, $expectation);
121+
}
122+
123+
/**
124+
* @return array
125+
*/
126+
public function productListSortOrderDataProvider(): array
127+
{
128+
return [
129+
'default_order_price_asc' => [
130+
'sort' => 'price',
131+
'direction' => 'asc',
132+
'expectation' => ['simple1', 'simple2', 'simple3'],
133+
],
134+
'default_order_price_desc' => [
135+
'sort' => 'price',
136+
'direction' => 'desc',
137+
'expectation' => ['simple3', 'simple2', 'simple1'],
138+
],
139+
'default_order_position_asc' => [
140+
'sort' => 'position',
141+
'direction' => 'asc',
142+
'expectation' => ['simple1', 'simple2', 'simple3'],
143+
],
144+
'default_order_position_desc' => [
145+
'sort' => 'position',
146+
'direction' => 'desc',
147+
'expectation' => ['simple3', 'simple2', 'simple1'],
148+
],
149+
'default_order_name_asc' => [
150+
'sort' => 'name',
151+
'direction' => 'asc',
152+
'expectation' => ['simple1', 'simple2', 'simple3'],
153+
],
154+
'default_order_name_desc' => [
155+
'sort' => 'name',
156+
'direction' => 'desc',
157+
'expectation' => ['simple3', 'simple2', 'simple1'],
158+
],
159+
'default_order_custom_attribute_asc' => [
160+
'sort' => 'test_configurable',
161+
'direction' => 'asc',
162+
'expectation' => ['simple1', 'simple3', 'simple2'],
163+
],
164+
'default_order_custom_attribute_desc' => [
165+
'sort' => 'test_configurable',
166+
'direction' => 'desc',
167+
'expectation' => ['simple3', 'simple2', 'simple1'],
168+
],
169+
];
170+
}
171+
172+
/**
173+
* @magentoDataFixture Magento/Store/_files/second_store.php
174+
* @magentoDataFixture Magento/Catalog/_files/products_with_not_empty_layered_navigation_attribute.php
175+
* @dataProvider productListSortOrderDataProviderOnStoreView
176+
* @param string $sortBy
177+
* @param string $direction
178+
* @param array $expectation
179+
* @param string $defaultSortBy
180+
* @return void
181+
*/
182+
public function testProductListSortOrderOnStoreView(
183+
string $sortBy,
184+
string $direction,
185+
array $expectation,
186+
string $defaultSortBy
187+
): void {
188+
$secondStoreId = (int)$this->storeManager->getStore('fixture_second_store')->getId();
189+
$this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, $defaultSortBy);
190+
$category = $this->updateCategorySortBy('Category 1', $secondStoreId, $sortBy);
191+
$this->renderBlock($category, $direction);
192+
$this->assertBlockSorting($sortBy, $expectation);
193+
}
194+
195+
/**
196+
* @magentoDataFixture Magento/Store/_files/second_store.php
197+
* @magentoDataFixture Magento/Catalog/_files/products_with_not_empty_layered_navigation_attribute.php
198+
* @dataProvider productListSortOrderDataProviderOnStoreView
199+
* @param string $sortBy
200+
* @param string $direction
201+
* @param array $expectation
202+
* @param string $defaultSortBy
203+
* @return void
204+
*/
205+
public function testProductListSortOrderWithConfigOnStoreView(
206+
string $sortBy,
207+
string $direction,
208+
array $expectation,
209+
string $defaultSortBy
210+
): void {
211+
$this->objectManager->removeSharedInstance(Config::class);
212+
$secondStoreId = (int)$this->storeManager->getStore('fixture_second_store')->getId();
213+
$this->scopeConfig->setValue(
214+
Config::XML_PATH_LIST_DEFAULT_SORT_BY,
215+
$defaultSortBy,
216+
ScopeInterface::SCOPE_STORE,
217+
Store::DEFAULT_STORE_ID
218+
);
219+
$this->scopeConfig->setValue(
220+
Config::XML_PATH_LIST_DEFAULT_SORT_BY,
221+
$sortBy,
222+
ScopeInterface::SCOPE_STORE,
223+
'fixture_second_store'
224+
);
225+
$this->updateCategorySortBy('Category 1', Store::DEFAULT_STORE_ID, null);
226+
$category = $this->updateCategorySortBy('Category 1', $secondStoreId, null);
227+
$this->renderBlock($category, $direction);
228+
$this->assertBlockSorting($sortBy, $expectation);
229+
}
230+
231+
/**
232+
* @return array
233+
*/
234+
public function productListSortOrderDataProviderOnStoreView(): array
235+
{
236+
return array_merge_recursive(
237+
$this->productListSortOrderDataProvider(),
238+
[
239+
'default_order_price_asc' => ['default_sort' => 'position'],
240+
'default_order_price_desc' => ['default_sort' => 'position'],
241+
'default_order_position_asc' => ['default_sort' => 'price'],
242+
'default_order_position_desc' => ['default_sort' => 'price'],
243+
'default_order_name_asc' => ['default_sort' => 'price'],
244+
'default_order_name_desc' => ['default_sort' => 'price'],
245+
'default_order_custom_attribute_asc' => ['default_sort' => 'price'],
246+
'default_order_custom_attribute_desc' => ['default_sort' => 'price'],
247+
]
248+
);
249+
}
250+
251+
/**
252+
* Renders block to apply sorting.
253+
*
254+
* @param CategoryInterface $category
255+
* @param string $direction
256+
* @return void
257+
*/
258+
private function renderBlock(CategoryInterface $category, string $direction): void
259+
{
260+
$this->block->getLayer()->setCurrentCategory($category);
261+
$this->block->setDefaultDirection($direction);
262+
$this->block->toHtml();
263+
}
264+
265+
/**
266+
* Checks product list block correct sorting.
267+
*
268+
* @param string $sortBy
269+
* @param array $expectation
270+
* @return void
271+
*/
272+
private function assertBlockSorting(string $sortBy, array $expectation): void
273+
{
274+
$this->assertArrayHasKey($sortBy, $this->block->getAvailableOrders());
275+
$this->assertEquals($sortBy, $this->block->getSortBy());
276+
$this->assertEquals($expectation, $this->block->getLoadedProductCollection()->getColumnValues('sku'));
277+
}
278+
279+
/**
280+
* Loads category by name.
281+
*
282+
* @param string $categoryName
283+
* @param int $storeId
284+
* @return CategoryInterface
285+
*/
286+
private function loadCategory(string $categoryName, int $storeId): CategoryInterface
287+
{
288+
/** @var Collection $categoryCollection */
289+
$categoryCollection = $this->categoryCollectionFactory->create();
290+
$categoryId = $categoryCollection->setStoreId($storeId)
291+
->addAttributeToFilter(CategoryInterface::KEY_NAME, $categoryName)
292+
->setPageSize(1)
293+
->getFirstItem()
294+
->getId();
295+
296+
return $this->categoryRepository->get($categoryId, $storeId);
297+
}
298+
299+
/**
300+
* Updates category default sort by field.
301+
*
302+
* @param string $categoryName
303+
* @param int $storeId
304+
* @param string|null $sortBy
305+
* @return CategoryInterface
306+
*/
307+
private function updateCategorySortBy(
308+
string $categoryName,
309+
int $storeId,
310+
?string $sortBy
311+
): CategoryInterface {
312+
$oldStoreId = $this->storeManager->getStore()->getId();
313+
$this->storeManager->setCurrentStore($storeId);
314+
$category = $this->loadCategory($categoryName, $storeId);
315+
$category->addData(['default_sort_by' => $sortBy]);
316+
$category = $this->categoryRepository->save($category);
317+
$this->storeManager->setCurrentStore($oldStoreId);
318+
319+
return $category;
320+
}
321+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\CategoryRepositoryInterface;
9+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
12+
use Magento\Store\Model\Store;
13+
use Magento\Store\Model\StoreManagerInterface;
14+
use Magento\TestFramework\Helper\Bootstrap;
15+
16+
require __DIR__ . '/products_with_layered_navigation_attribute.php';
17+
18+
$objectManager = Bootstrap::getObjectManager();
19+
/** @var StoreManagerInterface $storeManager */
20+
$storeManager = $objectManager->get(StoreManagerInterface::class);
21+
/** @var ProductAttributeRepositoryInterface $attributeRepository */
22+
$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class);
23+
/** @var ProductRepositoryInterface $productRepository */
24+
$productRepository = $objectManager->create(ProductRepositoryInterface::class);
25+
/** @var CategoryRepositoryInterface $categoryRepository */
26+
$categoryRepository = $objectManager->create(CategoryRepositoryInterface::class);
27+
$attribute = $attributeRepository->get('test_configurable');
28+
29+
$firstProduct = $productRepository->get('simple1');
30+
$firstProduct->setData('test_configurable', $attribute->getSource()->getOptionId('Option 1'));
31+
$productRepository->save($firstProduct);
32+
33+
$secondProduct = $productRepository->get('simple2');
34+
$secondProduct->setData('test_configurable', $attribute->getSource()->getOptionId('Option 2'));
35+
$productRepository->save($secondProduct);
36+
37+
$thirdProduct = $productRepository->get('simple3');
38+
$thirdProduct->setData('test_configurable', $attribute->getSource()->getOptionId('Option 2'));
39+
$thirdProduct->setStatus(Status::STATUS_ENABLED);
40+
$productRepository->save($thirdProduct);
41+
42+
$oldStoreId = $storeManager->getStore()->getId();
43+
$storeManager->setCurrentStore(Store::DEFAULT_STORE_ID);
44+
$category->addData(['available_sort_by' => 'position,name,price,test_configurable']);
45+
try {
46+
$categoryRepository->save($category);
47+
} finally {
48+
$storeManager->setCurrentStore($oldStoreId);
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
require __DIR__ . '/products_with_layered_navigation_attribute_rollback.php';

0 commit comments

Comments
 (0)