1010
1111use Magento \Catalog \Api \CategoryRepositoryInterface ;
1212use Magento \Catalog \Model \Category as CategoryModel ;
13+ use Magento \Catalog \Model \Indexer \Category \Product as CategoryProductIndexer ;
14+ use Magento \Catalog \Model \Indexer \Product \Category as ProductCategoryIndexer ;
1315use Magento \Catalog \Model \ResourceModel \Category as CategoryResource ;
1416use Magento \Catalog \Model \ResourceModel \Category \Collection as CategoryCollection ;
1517use Magento \Catalog \Model \ResourceModel \Category \CollectionFactory as CategoryCollectionFactory ;
18+ use Magento \Catalog \Model \ResourceModel \Product as ProductResource ;
19+ use Magento \Catalog \Model \ResourceModel \Product \Collection as ProductCollection ;
1620use Magento \Framework \App \Filesystem \DirectoryList ;
1721use Magento \Framework \Filesystem ;
1822use Magento \Framework \Filesystem \Directory \WriteInterface ;
23+ use Magento \Framework \Indexer \IndexerInterface ;
24+ use Magento \Framework \Indexer \IndexerRegistry ;
1925use Magento \Framework \ObjectManagerInterface ;
2026use Magento \Framework \UrlInterface ;
27+ use Magento \Indexer \Cron \UpdateMview ;
2128use Magento \Store \Model \StoreManagerInterface ;
2229use Magento \TestFramework \Helper \Bootstrap ;
2330use PHPUnit \Framework \TestCase ;
2633 * Tests category resource model
2734 *
2835 * @see \Magento\Catalog\Model\ResourceModel\Category
36+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2937 */
3038class CategoryTest extends TestCase
3139{
@@ -54,6 +62,11 @@ class CategoryTest extends TestCase
5462 /** @var WriteInterface */
5563 private $ mediaDirectory ;
5664
65+ /**
66+ * @var ProductResource
67+ */
68+ private $ productResource ;
69+
5770 /**
5871 * @inheritdoc
5972 */
@@ -68,6 +81,7 @@ protected function setUp(): void
6881 $ this ->categoryCollection = $ this ->objectManager ->get (CategoryCollectionFactory::class)->create ();
6982 $ this ->filesystem = $ this ->objectManager ->get (Filesystem::class);
7083 $ this ->mediaDirectory = $ this ->filesystem ->getDirectoryWrite (DirectoryList::MEDIA );
84+ $ this ->productResource = Bootstrap::getObjectManager ()->get (ProductResource::class);
7185 }
7286
7387 /**
@@ -116,6 +130,128 @@ public function testAddImageForCategory(): void
116130 $ this ->assertFileExists ($ this ->mediaDirectory ->getAbsolutePath ($ imageRelativePath ));
117131 }
118132
133+ /**
134+ * Test that adding or removing products in a category should not trigger full reindex in scheduled update mode
135+ *
136+ * @magentoAppArea adminhtml
137+ * @magentoAppIsolation enabled
138+ * @magentoDbIsolation disabled
139+ * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php
140+ * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php
141+ * @magentoDataFixture Magento/Catalog/_files/catalog_category_product_reindex_all.php
142+ * @magentoDataFixture Magento/Catalog/_files/catalog_product_category_reindex_all.php
143+ * @magentoDataFixture Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php
144+ * @dataProvider catalogProductChangesWithScheduledUpdateDataProvider
145+ * @param array $products
146+ * @return void
147+ */
148+ public function testCatalogProductChangesWithScheduledUpdate (array $ products ): void
149+ {
150+ // products are ordered by entity_id DESC because their positions are same and equal to 0
151+ $ initialProducts = ['simple1002 ' , 'simple1001 ' , 'simple1000 ' ];
152+ $ defaultStoreId = (int ) $ this ->storeManager ->getDefaultStoreView ()->getId ();
153+ $ category = $ this ->getCategory (['name ' => 'Category 999 ' ]);
154+ $ expectedProducts = array_keys ($ products );
155+ $ productIdsBySkus = $ this ->productResource ->getProductsIdsBySkus ($ expectedProducts );
156+ $ postedProducts = [];
157+ foreach ($ products as $ sku => $ position ) {
158+ $ postedProducts [$ productIdsBySkus [$ sku ]] = $ position ;
159+ }
160+ $ category ->setPostedProducts ($ postedProducts );
161+ $ this ->categoryResource ->save ($ category );
162+ // Indices should not be invalidated when adding/removing/reordering products in a category.
163+ $ categoryProductIndexer = $ this ->getIndexer (CategoryProductIndexer::INDEXER_ID );
164+ $ this ->assertTrue (
165+ $ categoryProductIndexer ->isValid (),
166+ '"Indexed category/products association" indexer should not be invalidated. '
167+ );
168+ $ productCategoryIndexer = $ this ->getIndexer (ProductCategoryIndexer::INDEXER_ID );
169+ $ this ->assertTrue (
170+ $ productCategoryIndexer ->isValid (),
171+ '"Indexed product/categories association" indexer should not be invalidated. '
172+ );
173+ // catalog products is not update until partial reindex occurs
174+ $ collection = $ this ->getCategoryProducts ($ category , $ defaultStoreId );
175+ $ this ->assertEquals ($ initialProducts , $ collection ->getColumnValues ('sku ' ));
176+ // Execute MVIEW cron handler for cron job "indexer_update_all_views"
177+ /** @var $mViewCron UpdateMview */
178+ $ mViewCron = $ this ->objectManager ->create (UpdateMview::class);
179+ $ mViewCron ->execute ();
180+ $ collection = $ this ->getCategoryProducts ($ category , $ defaultStoreId );
181+ $ this ->assertEquals ($ expectedProducts , $ collection ->getColumnValues ('sku ' ));
182+ }
183+
184+ /**
185+ * @return array
186+ */
187+ public function catalogProductChangesWithScheduledUpdateDataProvider (): array
188+ {
189+ return [
190+ 'change products position ' => [
191+ [
192+ 'simple1002 ' => 1 ,
193+ 'simple1000 ' => 2 ,
194+ 'simple1001 ' => 3 ,
195+ ]
196+ ],
197+ 'Add new product ' => [
198+ [
199+ 'simple1002 ' => 1 ,
200+ 'simple1000 ' => 2 ,
201+ 'simple-1 ' => 3 ,
202+ 'simple1001 ' => 4 ,
203+ ]
204+ ],
205+ 'Delete product ' => [
206+ [
207+ 'simple1002 ' => 1 ,
208+ 'simple1000 ' => 2 ,
209+ ]
210+ ]
211+ ];
212+ }
213+
214+ /**
215+ * @param CategoryModel $category
216+ * @param int $defaultStoreId
217+ * @return ProductCollection
218+ */
219+ private function getCategoryProducts (CategoryModel $ category , int $ defaultStoreId )
220+ {
221+ /** @var ProductCollection $collection */
222+ $ collection = $ this ->objectManager ->create (ProductCollection::class);
223+ $ collection ->setStoreId ($ defaultStoreId );
224+ $ collection ->addCategoryFilter ($ category );
225+ $ collection ->addAttributeToSort ('position ' );
226+ return $ collection ;
227+ }
228+
229+ /**
230+ * @param array $filters
231+ * @return CategoryModel
232+ */
233+ private function getCategory (array $ filters ): CategoryModel
234+ {
235+ /** @var CategoryCollection $categoryCollection */
236+ $ categoryCollection = $ this ->objectManager ->create (CategoryCollection::class);
237+ foreach ($ filters as $ field => $ value ) {
238+ $ categoryCollection ->addFieldToFilter ($ field , $ value );
239+ }
240+
241+ return $ categoryCollection ->getFirstItem ();
242+ }
243+
244+ /**
245+ * @param string $indexerId
246+ * @return IndexerInterface
247+ */
248+ private function getIndexer (string $ indexerId ): IndexerInterface
249+ {
250+ /** @var IndexerRegistry $indexerRegistry */
251+ $ indexerRegistry = $ this ->objectManager ->get (IndexerRegistry::class);
252+ return $ indexerRegistry ->get ($ indexerId );
253+ }
254+
119255 /**
120256 * Prepare image url for image data
121257 *
0 commit comments