diff --git a/Plugin/Catalog/Category/View.php b/Plugin/Catalog/Block/Product/ProductList/SetCollection.php similarity index 82% rename from Plugin/Catalog/Category/View.php rename to Plugin/Catalog/Block/Product/ProductList/SetCollection.php index 5e66090..6974acd 100644 --- a/Plugin/Catalog/Category/View.php +++ b/Plugin/Catalog/Block/Product/ProductList/SetCollection.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace SamJUK\FetchPriority\Plugin\Catalog\Category; +namespace SamJUK\FetchPriority\Plugin\Catalog\Block\Product\ProductList; +use Magento\Catalog\Block\Product\ProductList\Toolbar; use Magento\Catalog\Model\View\Asset\ImageFactory as AssetImageFactory; use Magento\Catalog\Model\Product\Image\ParamsBuilder; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\View\ConfigInterface; use Magento\Catalog\Helper\Image as ImageHelper; -class View +class SetCollection { public function __construct( private readonly \SamJUK\FetchPriority\Model\LinkStore $linkStore, private readonly \SamJUK\FetchPriority\Model\Links\PreloadFactory $preloadFactory, - private readonly \Magento\Catalog\Block\Product\ListProduct $listProductBlock, private readonly ConfigInterface $presentationConfig, private readonly ParamsBuilder $imageParamsBuilder, private readonly PlaceholderFactory $viewAssetPlaceholderFactory, @@ -23,21 +23,20 @@ public function __construct( private readonly \SamJUK\FetchPriority\Model\Config $config ) { } - public function afterExecute($subject, $result) + public function afterSetCollection(Toolbar $subject, $result, $collection) { if (!$this->config->isEnabled() || !$this->config->isCategoryProductPreloadEnabled()) { return $result; } - $this->preloadInitialProductImages(); + $this->preloadInitialProductImages($collection, $subject->getCurrentMode()); return $result; } - public function preloadInitialProductImages() + private function preloadInitialProductImages($collection, string $mode): void { $i = 0; - $imageType = $this->getImageType(); - $collection = $this->listProductBlock->getLoadedProductCollection(); + $imageType = $this->getImageType($mode); foreach ($collection as $product) { if (++$i > 4) { return; @@ -76,11 +75,9 @@ private function getProductImage($product, $imageType) return $imageAsset->getUrl(); } - private function getImageType() + private function getImageType(string $mode) { - return $this->listProductBlock->getMode() === 'grid' - ? 'category_page_grid' - : 'category_page_list'; + return $mode === 'grid' ? 'category_page_grid' : 'category_page_list'; } private function preload(string $imageUrl) diff --git a/Test/Unit/Plugin/Catalog/Block/Product/ProductList/SetCollectionTest.php b/Test/Unit/Plugin/Catalog/Block/Product/ProductList/SetCollectionTest.php new file mode 100644 index 0000000..78395b5 --- /dev/null +++ b/Test/Unit/Plugin/Catalog/Block/Product/ProductList/SetCollectionTest.php @@ -0,0 +1,147 @@ +preloadFactoryMock = $this->getMockBuilder(PreloadFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->getMock(); + + $this->linkStoreMock = $this->createMock(LinkStore::class); + $this->presentationConfigMock = $this->createMock(ConfigInterface::class); + $this->imageParamsBuilderMock = $this->createMock(ParamsBuilder::class); + $this->viewAssetPlaceholderFactoryMock = $this->createMock(PlaceholderFactory::class); + $this->viewAssetImageFactoryMock = $this->createMock(AssetImageFactory::class); + $this->configMock = $this->createMock(Config::class); + $this->toolbarMock = $this->createMock(Toolbar::class); + + $viewConfigMock = $this->createMock(ViewConfig::class); + $viewConfigMock->method('getMediaAttributes')->willReturn(['image_type' => 'small_image']); + $this->presentationConfigMock->method('getViewConfig')->willReturn($viewConfigMock); + $this->imageParamsBuilderMock->method('build')->willReturn(['image_type' => 'small_image']); + + $this->subject = new SetCollection( + $this->linkStoreMock, + $this->preloadFactoryMock, + $this->presentationConfigMock, + $this->imageParamsBuilderMock, + $this->viewAssetPlaceholderFactoryMock, + $this->viewAssetImageFactoryMock, + $this->configMock + ); + } + + public function testDoesNothingWhenModuleDisabled(): void + { + $this->configMock->method('isEnabled')->willReturn(false); + $this->configMock->method('isCategoryProductPreloadEnabled')->willReturn(true); + + $this->linkStoreMock->expects($this->never())->method('add'); + + $this->subject->afterSetCollection($this->toolbarMock, $this->toolbarMock, []); + } + + public function testDoesNothingWhenCategoryPreloadDisabled(): void + { + $this->configMock->method('isEnabled')->willReturn(true); + $this->configMock->method('isCategoryProductPreloadEnabled')->willReturn(false); + + $this->linkStoreMock->expects($this->never())->method('add'); + + $this->subject->afterSetCollection($this->toolbarMock, $this->toolbarMock, []); + } + + public function testPreloadsProductsFromAlreadySortedCollection(): void + { + $this->configMock->method('isEnabled')->willReturn(true); + $this->configMock->method('isCategoryProductPreloadEnabled')->willReturn(true); + $this->toolbarMock->method('getCurrentMode')->willReturn('grid'); + + $productMock = $this->createMock(Product::class); + $productMock->method('getData')->with('small_image')->willReturn('product-image.jpg'); + + $imageAssetMock = $this->createMock(ViewAssetImage::class); + $imageAssetMock->method('getUrl')->willReturn('https://example.com/media/product-image.jpg'); + $this->viewAssetImageFactoryMock->method('create')->willReturn($imageAssetMock); + + $preloadMock = $this->createMock(Preload::class); + $this->preloadFactoryMock->expects($this->once()) + ->method('create') + ->with($this->callback(function ($args) { + return $args['href'] === 'https://example.com/media/product-image.jpg'; + })) + ->willReturn($preloadMock); + + $this->linkStoreMock->expects($this->once())->method('add')->with($preloadMock); + + // The collection is passed as the setCollection() argument, which by the time + // this plugin runs (afterSetCollection) already has sort order/pagination applied + // by the original method — iterating it here does not race the toolbar's ordering. + $result = $this->subject->afterSetCollection($this->toolbarMock, $this->toolbarMock, [$productMock]); + + $this->assertSame($this->toolbarMock, $result); + } + + public function testStopsPreloadingAfterFourProducts(): void + { + $this->configMock->method('isEnabled')->willReturn(true); + $this->configMock->method('isCategoryProductPreloadEnabled')->willReturn(true); + $this->toolbarMock->method('getCurrentMode')->willReturn('list'); + + $products = []; + for ($i = 0; $i < 6; $i++) { + $productMock = $this->createMock(Product::class); + $productMock->method('getData')->with('small_image')->willReturn("product-{$i}.jpg"); + $products[] = $productMock; + } + + $imageAssetMock = $this->createMock(ViewAssetImage::class); + $imageAssetMock->method('getUrl')->willReturn('https://example.com/media/product.jpg'); + $this->viewAssetImageFactoryMock->method('create')->willReturn($imageAssetMock); + + $preloadMock = $this->createMock(Preload::class); + $this->preloadFactoryMock->method('create')->willReturn($preloadMock); + + $this->linkStoreMock->expects($this->exactly(4))->method('add'); + + $this->subject->afterSetCollection($this->toolbarMock, $this->toolbarMock, $products); + } +} diff --git a/etc/di.xml b/etc/di.xml index e114b2e..8e755d0 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -1,6 +1,6 @@ - - + +