Skip to content

Commit aa21702

Browse files
committed
AC-10659 : Category Products Grid > Status & Visibility Columns are empty when sorting by name
1 parent 032ee42 commit aa21702

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Category\Tab;
9+
10+
use Magento\Backend\Helper\Data as BackendHelper;
11+
use Magento\Backend\Model\Session as BackendSession;
12+
use Magento\Catalog\Block\Adminhtml\Category\Tab\Product as CategoryTabProductBlock;
13+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
14+
use Magento\Catalog\Model\Product\Visibility;
15+
use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
16+
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
17+
use Magento\Framework\App\Request\Http as HttpRequest;
18+
use Magento\Framework\DB\Select;
19+
use PHPUnit\Framework\TestCase;
20+
use PHPUnit\Framework\MockObject\MockObject;
21+
use Magento\Framework\Math\Random;
22+
23+
class ProductBlockPrepareCollectionTest extends TestCase
24+
{
25+
/**
26+
* @var BackendHelper|MockObject
27+
*/
28+
private $backendHelperMock;
29+
30+
/**
31+
* @var BackendSession|MockObject
32+
*/
33+
private $backendSessionMock;
34+
35+
/**
36+
* @var (HttpRequest&MockObject)|MockObject
37+
*/
38+
private $requestMock;
39+
40+
/**
41+
* @var ProductCollectionFactory|MockObject
42+
*/
43+
private $collectionFactoryMock;
44+
45+
/**
46+
* @var ProductCollection|MockObject
47+
*/
48+
private $collectionMock;
49+
50+
/**
51+
* @var Select|MockObject
52+
*/
53+
private $selectMock;
54+
55+
/**
56+
* @var Visibility|MockObject
57+
*/
58+
private $visibilityMock;
59+
60+
/**
61+
* @var Status|MockObject
62+
*/
63+
private $statusMock;
64+
65+
/**
66+
* @var MockObject
67+
*/
68+
private $mathRandomMock;
69+
70+
protected function setUp(): void
71+
{
72+
$this->backendHelperMock = $this->createMock(BackendHelper::class);
73+
$this->backendSessionMock = $this->createMock(BackendSession::class);
74+
75+
// Use Http request so getPost() exists
76+
$this->requestMock = $this->getMockBuilder(HttpRequest::class)
77+
->disableOriginalConstructor()
78+
->onlyMethods(['getParam', 'getPost', 'has'])
79+
->getMock();
80+
81+
$this->collectionFactoryMock = $this->createMock(ProductCollectionFactory::class);
82+
$this->collectionMock = $this->createMock(ProductCollection::class);
83+
$this->selectMock = $this->createMock(Select::class);
84+
85+
$this->visibilityMock = $this->createMock(Visibility::class);
86+
$this->statusMock = $this->createMock(Status::class);
87+
88+
// mathRandom for getId() inside Grid::getParam()
89+
$this->mathRandomMock = $this->getMockBuilder(Random::class)
90+
->disableOriginalConstructor()
91+
->onlyMethods(['getUniqueHash'])
92+
->getMock();
93+
$this->mathRandomMock->method('getUniqueHash')->willReturn('id_test');
94+
95+
$this->collectionFactoryMock->method('create')->willReturn($this->collectionMock);
96+
97+
// Fluent + no-op DB calls
98+
$this->collectionMock->method('addAttributeToSelect')->willReturnSelf();
99+
$this->collectionMock->method('addStoreFilter')->willReturnSelf();
100+
$this->collectionMock->method('joinField')->willReturnSelf();
101+
$this->collectionMock->method('getSelect')->willReturn($this->selectMock);
102+
$this->collectionMock->method('load')->willReturnSelf();
103+
$this->selectMock->method('group')->willReturnSelf();
104+
105+
$this->requestMock->method('has')->willReturn(false);
106+
}
107+
108+
private function buildBlock(array $methodsToMock, object $categoryStub): CategoryTabProductBlock
109+
{
110+
$block = $this->getMockBuilder(CategoryTabProductBlock::class)
111+
->disableOriginalConstructor()
112+
->onlyMethods($methodsToMock)
113+
->getMock();
114+
115+
// private Product::$productCollectionFactory
116+
$declProduct = new \ReflectionClass(CategoryTabProductBlock::class);
117+
$propPcf = $declProduct->getProperty('productCollectionFactory');
118+
$propPcf->setAccessible(true);
119+
$propPcf->setValue($block, $this->collectionFactoryMock);
120+
121+
// Grid protected deps
122+
$declGrid = new \ReflectionClass(\Magento\Backend\Block\Widget\Grid::class);
123+
124+
$bhProp = $declGrid->getProperty('_backendHelper');
125+
$bhProp->setAccessible(true);
126+
$bhProp->setValue($block, $this->backendHelperMock);
127+
128+
$bsProp = $declGrid->getProperty('_backendSession');
129+
$bsProp->setAccessible(true);
130+
$bsProp->setValue($block, $this->backendSessionMock);
131+
132+
// AbstractBlock::_request
133+
$declAbs = new \ReflectionClass(\Magento\Framework\View\Element\AbstractBlock::class);
134+
$reqProp = $declAbs->getProperty('_request');
135+
$reqProp->setAccessible(true);
136+
$reqProp->setValue($block, $this->requestMock);
137+
138+
// Backend\Block\Template::$mathRandom for getId() calls in Grid::getParam()
139+
$declTpl = new \ReflectionClass(\Magento\Backend\Block\Template::class);
140+
$mrProp = $declTpl->getProperty('mathRandom');
141+
$mrProp->setAccessible(true);
142+
$mrProp->setValue($block, $this->mathRandomMock);
143+
144+
// Avoid Grid column lookups
145+
if (in_array('getColumn', $methodsToMock, true)) {
146+
$block->method('getColumn')->willReturn(null);
147+
}
148+
// Provide category
149+
if (in_array('getCategory', $methodsToMock, true)) {
150+
$block->method('getCategory')->willReturn($categoryStub);
151+
}
152+
153+
return $block;
154+
}
155+
156+
public function testPrepareCollectionWithCategoryIdAndNoStore(): void
157+
{
158+
$categoryStub = new class {
159+
public function getId()
160+
{
161+
return 42;
162+
}
163+
public function getProductsReadonly()
164+
{
165+
return false;
166+
}
167+
};
168+
169+
$this->requestMock->method('getParam')
170+
->willReturnMap([
171+
['store', 0, 0],
172+
['id', 0, 42],
173+
]);
174+
175+
$this->collectionMock->expects($this->once())
176+
->method('addAttributeToSelect')
177+
->with(
178+
$this->callback(fn($attrs) =>
179+
is_array($attrs) && in_array('name', $attrs, true) && in_array('price', $attrs, true)),
180+
'left'
181+
)
182+
->willReturnSelf();
183+
184+
$this->collectionMock->expects($this->once())
185+
->method('joinField')
186+
->with(
187+
'position',
188+
'catalog_category_product',
189+
'position',
190+
'product_id=entity_id',
191+
'category_id=42',
192+
'left'
193+
)
194+
->willReturnSelf();
195+
196+
$block = $this->buildBlock(['getCategory', 'getColumn', 'setDefaultFilter'], $categoryStub);
197+
$block->expects($this->once())->method('setDefaultFilter')->with(['in_category' => 1])->willReturnSelf();
198+
199+
$this->invokePrepareCollection($block);
200+
201+
$this->assertSame($this->collectionMock, $block->getCollection());
202+
}
203+
204+
public function testPrepareCollectionWithStoreAndReadonly(): void
205+
{
206+
$categoryStub = new class {
207+
public function getId()
208+
{
209+
return null;
210+
}
211+
public function getProductsReadonly()
212+
{
213+
return true;
214+
}
215+
public function getProductsPosition()
216+
{
217+
return [5 => 10, 7 => 20];
218+
}
219+
};
220+
221+
$this->requestMock->method('getParam')
222+
->willReturnMap([
223+
['store', 0, 3],
224+
['id', 0, 99],
225+
]);
226+
$this->requestMock->method('getPost')->willReturn(null);
227+
228+
$this->collectionMock->expects($this->once())->method('addStoreFilter')->with(3)->willReturnSelf();
229+
230+
$this->collectionMock->expects($this->once())
231+
->method('joinField')
232+
->with(
233+
'position',
234+
'catalog_category_product',
235+
'position',
236+
'product_id=entity_id',
237+
'category_id=99',
238+
'left'
239+
)
240+
->willReturnSelf();
241+
242+
$this->collectionMock->expects($this->once())
243+
->method('addFieldToFilter')
244+
->with('entity_id', ['in' => [5, 7]])
245+
->willReturnSelf();
246+
247+
$block = $this->buildBlock(['getCategory', 'getColumn'], $categoryStub);
248+
249+
$this->invokePrepareCollection($block);
250+
251+
$this->assertSame($this->collectionMock, $block->getCollection());
252+
}
253+
254+
private function invokePrepareCollection(object $block): void
255+
{
256+
$m = (new \ReflectionClass($block))->getMethod('_prepareCollection');
257+
$m->setAccessible(true);
258+
$m->invoke($block);
259+
}
260+
}

0 commit comments

Comments
 (0)