diff --git a/README.md b/README.md
index 842e26b0..0034f2c7 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ Below is a rundown of all configuration options
`www.example.com/example-category?color=red`.
If Seo path slugs is selected the url is constructed as `www.example.com/example-category/color/red`.
+8) Use category URL from Tweakwise: When enabled, category facet links use the URL provided by Tweakwise in the navigation response instead of URLs generated by Magento. This reduces server-side computation and makes category URLs more predictable. Only applies on category pages, not on search results. Falls back to the standard Magento-generated URL when Tweakwise does not provide a link for an item.
#### Seo (All settings depend on Enabled having value yes)
1) Enabled: use Seo options yes or no.
diff --git a/src/Block/Catalog/Product/ProductList/Toolbar/Plugin.php b/src/Block/Catalog/Product/ProductList/Toolbar/Plugin.php
index f36e0c5c..697ac5fe 100644
--- a/src/Block/Catalog/Product/ProductList/Toolbar/Plugin.php
+++ b/src/Block/Catalog/Product/ProductList/Toolbar/Plugin.php
@@ -87,7 +87,7 @@ public function aroundGetAvailableOrders(Toolbar $subject, Closure $proceed)
}
/** @var SortFieldType[] $sortFields */
- $sortFields = $this->context->getResponse()->getProperties()->getSortFields(); // @phpstan-ignore-line
+ $sortFields = $this->context->getResponse()->getProperties()->getSortFields() ?? []; // @phpstan-ignore-line
$result = [];
foreach ($sortFields as $field) {
diff --git a/src/Block/LayeredNavigation/RenderLayered/DefaultRenderer.php b/src/Block/LayeredNavigation/RenderLayered/DefaultRenderer.php
index 51a21424..ad20e126 100644
--- a/src/Block/LayeredNavigation/RenderLayered/DefaultRenderer.php
+++ b/src/Block/LayeredNavigation/RenderLayered/DefaultRenderer.php
@@ -108,8 +108,13 @@ public function getCategoryUrl(Item $item): string
{
$catUrl = $item->getUrl();
- if (strpos($catUrl, $this->getBaseUrl()) === false) {
- $catUrl = $this->getBaseUrl() . $item->getUrl();
+ // Compare scheme-agnostically to handle http vs https mismatches between
+ // the Tweakwise-provided link and the Magento base URL.
+ $catUrlWithoutScheme = preg_replace('#^https?://#', '', $catUrl);
+ $baseUrlWithoutScheme = preg_replace('#^https?://#', '', $this->getBaseUrl());
+
+ if (strpos($catUrlWithoutScheme, $baseUrlWithoutScheme) === false) {
+ $catUrl = rtrim($this->getBaseUrl(), '/') . '/' . ltrim($catUrl, '/');
}
return $catUrl;
diff --git a/src/Model/Catalog/Layer/Filter.php b/src/Model/Catalog/Layer/Filter.php
index 16173b07..453b5e40 100644
--- a/src/Model/Catalog/Layer/Filter.php
+++ b/src/Model/Catalog/Layer/Filter.php
@@ -356,7 +356,6 @@ protected function createItem(AttributeType $attributeType)
$item = $this->itemFactory->create(['filter' => $this, 'attributeType' => $attributeType]);
$children = [];
- // @phpstan-ignore-next-line
foreach ($attributeType->getChildren() as $childAttributeType) {
$children[] = $this->createItem($childAttributeType);
}
diff --git a/src/Model/Catalog/Layer/NavigationContext/CurrentContext.php b/src/Model/Catalog/Layer/NavigationContext/CurrentContext.php
index 0434d29e..fe0abb8a 100644
--- a/src/Model/Catalog/Layer/NavigationContext/CurrentContext.php
+++ b/src/Model/Catalog/Layer/NavigationContext/CurrentContext.php
@@ -71,7 +71,7 @@ public function getResponse()
*/
public function getTweakwiseRequestId(): string
{
- $headers = $this->getContext()->getResponse()->getValue('headers');
+ $headers = $this->getContext()->getResponse()->getValue('headers') ?? [];
$normalized = array_change_key_case($headers, CASE_LOWER);
return $normalized['twn-request-id'][0] ?? '';
diff --git a/src/Model/Catalog/Layer/Url.php b/src/Model/Catalog/Layer/Url.php
index 2c4925f6..c1a7ff50 100644
--- a/src/Model/Catalog/Layer/Url.php
+++ b/src/Model/Catalog/Layer/Url.php
@@ -10,18 +10,22 @@
namespace Tweakwise\Magento2Tweakwise\Model\Catalog\Layer;
use Tweakwise\Magento2Tweakwise\Model\Catalog\Layer\Filter\Item;
+use Tweakwise\Magento2Tweakwise\Model\Catalog\Layer\NavigationContext\CurrentContext;
use Tweakwise\Magento2Tweakwise\Model\Catalog\Layer\Url\CategoryUrlInterface;
use Tweakwise\Magento2Tweakwise\Model\Catalog\Layer\Url\FilterApplierInterface;
use Tweakwise\Magento2Tweakwise\Model\Catalog\Layer\Url\Strategy\UrlStrategyFactory;
use Tweakwise\Magento2Tweakwise\Model\Catalog\Layer\Url\UrlInterface;
use Tweakwise\Magento2Tweakwise\Model\Catalog\Layer\Url\UrlModel;
use Tweakwise\Magento2Tweakwise\Model\Client\Request\ProductNavigationRequest;
+use Tweakwise\Magento2Tweakwise\Model\Client\Request\ProductSearchRequest;
use Tweakwise\Magento2Tweakwise\Model\Client\Type\FacetType\SettingsType;
+use Tweakwise\Magento2Tweakwise\Model\Config;
use Tweakwise\Magento2TweakwiseExport\Model\Helper as ExportHelper;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Framework\App\Request\Http as MagentoHttpRequest;
use Magento\Framework\Exception\NoSuchEntityException;
+use Exception;
/**
* Class Url will later implement logic to use implementation selected in configuration.
@@ -72,13 +76,18 @@ class Url
* @param MagentoHttpRequest $request
* @param CategoryRepositoryInterface $categoryRepository
* @param ExportHelper $exportHelper
+ * @param UrlModel $magentoUrl
+ * @param Config $config
+ * @param CurrentContext $currentContext
*/
public function __construct(
UrlStrategyFactory $urlStrategyFactory,
MagentoHttpRequest $request,
CategoryRepositoryInterface $categoryRepository,
ExportHelper $exportHelper,
- UrlModel $magentoUrl
+ UrlModel $magentoUrl,
+ protected readonly Config $config,
+ protected readonly CurrentContext $currentContext
) {
$this->urlStrategyFactory = $urlStrategyFactory;
$this->categoryRepository = $categoryRepository;
@@ -143,6 +152,10 @@ public function getSelectFilter(Item $item): string
->getFacetSettings();
if ($settings->getSource() === SettingsType::SOURCE_CATEGORY) {
+ if ($this->shouldUseTweakwiseCategoryUrl($item)) {
+ return $item->getAttribute()->getLink();
+ }
+
return $this->getCategoryUrlStrategy()
->getCategoryFilterSelectUrl($this->request, $item);
}
@@ -150,6 +163,36 @@ public function getSelectFilter(Item $item): string
return $this->addBaseUrl($this->getUrlStrategy()->getAttributeSelectUrl($this->request, $item));
}
+ /**
+ * Determine whether the Tweakwise-provided category URL should be used for a filter item.
+ * Returns true when:
+ * - the "Use category URL from Tweakwise" setting is enabled
+ * - the item carries a non-empty link from Tweakwise
+ * - the current request is not a search request
+ *
+ * @param Item $item
+ * @return bool
+ */
+ protected function shouldUseTweakwiseCategoryUrl(Item $item): bool
+ {
+ if (!$this->config->isCategoryUrlFromTweakwiseEnabled()) {
+ return false;
+ }
+
+ $tweakwiseUrl = $item->getAttribute()->getLink();
+ if (empty($tweakwiseUrl)) {
+ return false;
+ }
+
+ try {
+ $navigationRequest = $this->currentContext->getRequest();
+ } catch (Exception $e) {
+ return false;
+ }
+
+ return !($navigationRequest instanceof ProductSearchRequest);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Model/Client/Type/AttributeType.php b/src/Model/Client/Type/AttributeType.php
index 5e577b04..6ef87339 100644
--- a/src/Model/Client/Type/AttributeType.php
+++ b/src/Model/Client/Type/AttributeType.php
@@ -33,11 +33,11 @@ public function setChildren(array $children)
}
/**
- * @return string
+ * @return AttributeType[]
*/
- public function getChildren()
+ public function getChildren(): array
{
- return $this->getDataValue('children');
+ return $this->getDataValue('children') ?? [];
}
/**
@@ -82,6 +82,14 @@ public function getUrl()
return (string) $this->getDataValue('url');
}
+ /**
+ * @return string
+ */
+ public function getLink(): string
+ {
+ return (string) $this->getDataValue('link');
+ }
+
/**
* @return int|null
*/
diff --git a/src/Model/Config.php b/src/Model/Config.php
index b7cfa4a6..033523ee 100644
--- a/src/Model/Config.php
+++ b/src/Model/Config.php
@@ -663,6 +663,15 @@ public function isCategoryViewDefault(?Store $store = null)
return $this->getStoreConfig('tweakwise/layered/default_category_view', $store);
}
+ /**
+ * @param Store|null $store
+ * @return bool
+ */
+ public function isCategoryUrlFromTweakwiseEnabled(?Store $store = null): bool
+ {
+ return (bool) $this->getStoreConfig('tweakwise/layered/use_category_url_from_tweakwise', $store);
+ }
+
/**
* @return int
*/
diff --git a/src/etc/adminhtml/system.xml b/src/etc/adminhtml/system.xml
index 3c1c19ff..78d81aeb 100644
--- a/src/etc/adminhtml/system.xml
+++ b/src/etc/adminhtml/system.xml
@@ -116,6 +116,14 @@
0
+
+
+ When enabled, category facet links use the URL provided by Tweakwise instead of Magento-generated URLs. Only applies on category pages, not on search results.
+ Magento\Config\Model\Config\Source\Yesno
+
+ 1
+
+
diff --git a/tests/Functional/CategoryLinkTest.php b/tests/Functional/CategoryLinkTest.php
new file mode 100644
index 00000000..af6e2f9d
--- /dev/null
+++ b/tests/Functional/CategoryLinkTest.php
@@ -0,0 +1,199 @@
+category = new CategoryFixture('Women Tops');
+ $this->tester->createFixture($this->category);
+
+ $this->tester->mockConfig('tweakwise/general/enabled', '1');
+ $this->tester->mockConfig('tweakwise/layered/enabled', '1');
+ }
+
+ /**
+ * When the setting is disabled, category facet links must resolve to the store domain,
+ * not the raw Tweakwise link field.
+ *
+ * @return void
+ */
+ public function testCategoryFacetLinksUseMagentoUrlWhenSettingDisabled(): void
+ {
+ $this->tester->mockConfig('tweakwise/layered/use_category_url_from_tweakwise', '0');
+ $this->mockClientWithCategoryFacet(self::TWEAKWISE_LINK);
+
+ $this->tester->amOnPage('/' . $this->category->getUrlKey() . '.html');
+
+ // Magento store domain must appear in the page (navigation rendered).
+ $this->tester->seeInSource('tweakwise.test');
+ // A double-domain href indicates the raw Tweakwise URL leaked into a Magento-built URL.
+ $this->tester->dontSeeInSource('tweakwise.test/default/http');
+ $this->tester->dontSeeInSource('tweakwise.test/default/https');
+ }
+
+ /**
+ * When the setting is enabled, category facet links must use the Tweakwise-provided link field.
+ *
+ * @return void
+ */
+ public function testCategoryFacetLinksUseTweakwiseUrlWhenSettingEnabled(): void
+ {
+ $this->tester->mockConfig('tweakwise/layered/use_category_url_from_tweakwise', '1');
+ $this->mockClientWithCategoryFacet(self::TWEAKWISE_LINK);
+
+ $this->tester->amOnPage('/' . $this->category->getUrlKey() . '.html');
+
+ $this->tester->seeInSource('tweakwise.test');
+ }
+
+ /**
+ * No duplicated domain must appear regardless of the setting value.
+ *
+ * @return void
+ */
+ public function testCategoryFacetLinksContainNoDuplicatedDomain(): void
+ {
+ $this->tester->mockConfig('tweakwise/layered/use_category_url_from_tweakwise', '1');
+ $this->mockClientWithCategoryFacet(self::TWEAKWISE_LINK);
+
+ $this->tester->amOnPage('/' . $this->category->getUrlKey() . '.html');
+
+ $this->tester->dontSeeInSource('tweakwise.testtweakwise.test');
+ $this->tester->dontSeeInSource('tweakwise.test//');
+ }
+
+ /**
+ * On the search results page the raw Tweakwise link field must never be used,
+ * even when the setting is enabled.
+ *
+ * @return void
+ */
+ public function testCategoryFacetLinksUseMagentoUrlOnSearchResultsPageEvenWhenEnabled(): void
+ {
+ $this->tester->mockConfig('tweakwise/layered/use_category_url_from_tweakwise', '1');
+ $this->mockClientWithCategoryFacet(self::TWEAKWISE_LINK);
+
+ $this->tester->amOnPage('/catalogsearch/result/?q=tops');
+
+ // Search page must not produce a raw category link href.
+ $this->tester->seeInSource('catalogsearch');
+ $this->tester->dontSeeInSource(self::TWEAKWISE_LINK);
+ }
+
+ /**
+ * When Tweakwise returns an empty link field the Magento URL must be used as fallback.
+ *
+ * @return void
+ */
+ public function testCategoryFacetLinkFallsBackToMagentoUrlWhenTweakwiseLinkIsEmpty(): void
+ {
+ $this->tester->mockConfig('tweakwise/layered/use_category_url_from_tweakwise', '1');
+ $this->mockClientWithCategoryFacet('');
+
+ $this->tester->amOnPage('/' . $this->category->getUrlKey() . '.html');
+
+ // With an empty link the rendered href must not contain a bare slash-slash or be empty.
+ $this->tester->dontSeeInSource('href=""');
+ $this->tester->dontSeeInSource('tweakwise.test//');
+ }
+
+ /**
+ * Build a mocked Client that returns a ProductNavigationResponse containing a single
+ * category facet with one attribute item whose link field equals $categoryLink.
+ *
+ * @param string $categoryLink Value for the `link` field on the attribute item.
+ * @return void
+ */
+ private function mockClientWithCategoryFacet(string $categoryLink): void
+ {
+ $response = $this->buildNavigationResponse($categoryLink);
+
+ $client = Mockery::mock(Client::class);
+ $client->shouldReceive('request')->andReturn($response);
+
+ $this->tester->mockService(Client::class, $client);
+ }
+
+ /**
+ * Build a minimal ProductNavigationResponse with one category facet containing one item.
+ *
+ * @param string $categoryLink
+ * @return ProductNavigationResponse
+ */
+ private function buildNavigationResponse(string $categoryLink): ProductNavigationResponse
+ {
+ $facetData = [
+ 'facetsettings' => [
+ 'source' => SettingsType::SOURCE_CATEGORY,
+ 'title' => 'Category',
+ 'urlkey' => 'cat',
+ 'selectiontype' => SettingsType::SELECTION_TYPE_LINK,
+ 'isnrofresultsvisible' => 'false',
+ 'ismultiselect' => 'false',
+ 'iscollapsible' => 'false',
+ 'iscollapsed' => 'false',
+ 'isinfivisible' => 'false',
+ 'isvisible' => 'true',
+ 'nrofshownattributes' => 10,
+ ],
+ 'attributes' => [
+ [
+ 'title' => 'Tops',
+ 'url' => 'https://tweakwise.test/default/women/tops-women2/',
+ 'link' => $categoryLink,
+ 'nrofresults' => 12,
+ 'isselected' => 'false',
+ 'attributeid' => '100042',
+ ],
+ ],
+ ];
+
+ $facet = new FacetType($facetData);
+
+ /** @var ProductNavigationResponse $response */
+ $response = $this->tester->getObjectManager()->create(
+ ProductNavigationResponse::class,
+ [
+ 'data' => [
+ 'facets' => [$facet],
+ 'items' => [],
+ 'properties' => [
+ 'nrofitems' => 0,
+ 'nrofpages' => 0,
+ 'currentpage' => 1,
+ 'nrofitemsperpage' => 16,
+ 'selectedcategoryid' => null,
+ ],
+ ],
+ ]
+ );
+
+ return $response;
+ }
+}
diff --git a/tests/Functional/InitialTest.php b/tests/Functional/InitialTest.php
deleted file mode 100644
index f71fd2fd..00000000
--- a/tests/Functional/InitialTest.php
+++ /dev/null
@@ -1,24 +0,0 @@
-tester->assertTrue(true);
- }
-}
diff --git a/tests/Unit.suite.yml b/tests/Unit.suite.yml
index 76c1b979..2ed0be8b 100644
--- a/tests/Unit.suite.yml
+++ b/tests/Unit.suite.yml
@@ -1,5 +1,48 @@
actor: UnitTester
suite_namespace: Tweakwise\Test\Unit
modules:
- # enable helpers as array
- enabled: []
+ enabled:
+ - Asserts
+ - \Emico\CodeCept\Module\Magento2
+ - \Emico\CodeCept\Module\Mocking
+ - \Emico\CodeCept\Module\Fixtures
+ - \Emico\CodeCept\Module\Config
+ - \Emico\CodeCept\Module\Queue
+ config:
+ \Emico\CodeCept\Module\Magento2:
+ url: 'https://tweakwise.test'
+ store_code: 'default'
+ configuration_bootstrap:
+ queue:
+ amqp:
+ host: 127.0.0.1
+ port: 5672
+ user: tweakwise
+ password: tweakwise
+ virtualhost: tweakwise
+ configuration:
+ default:
+ web:
+ unsecure:
+ base_url: 'https://tweakwise.test/'
+ secure:
+ base_url: 'https://tweakwise.test/'
+ websites:
+ magento:
+ web:
+ unsecure:
+ base_url: 'https://tweakwise.test/'
+ secure:
+ base_url: 'https://tweakwise.test/'
+ \Emico\CodeCept\Module\Config:
+ depends:
+ - \Emico\CodeCept\Module\Magento2
+ \Emico\CodeCept\Module\Mocking:
+ depends:
+ - \Emico\CodeCept\Module\Magento2
+ \Emico\CodeCept\Module\Fixtures:
+ depends:
+ - \Emico\CodeCept\Module\Magento2
+ \Emico\CodeCept\Module\Queue:
+ depends:
+ - \Emico\CodeCept\Module\Magento2
diff --git a/tests/Unit/Block/LayeredNavigation/RenderLayered/DefaultRendererTest.php b/tests/Unit/Block/LayeredNavigation/RenderLayered/DefaultRendererTest.php
new file mode 100644
index 00000000..0c6cfe81
--- /dev/null
+++ b/tests/Unit/Block/LayeredNavigation/RenderLayered/DefaultRendererTest.php
@@ -0,0 +1,81 @@
+mockRenderer();
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetCategoryUrlDoesNotPrependDomainWhenOnlySchemeDiffers(): void
+ {
+ $url = 'http://magento2.test/default/women/tops-women2/';
+ $item = $this->mockItem($url);
+
+ $this->assertEquals(
+ $url,
+ $this->renderer->getCategoryUrl($item)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetCategoryUrlPrependsBaseUrlForRelativeFacetLink(): void
+ {
+ $item = $this->mockItem('/default/women/tops-women2/');
+
+ $this->assertEquals(
+ 'https://magento2.test/default/women/tops-women2/',
+ $this->renderer->getCategoryUrl($item)
+ );
+ }
+
+ /**
+ * @return void
+ */
+ private function mockRenderer(): void
+ {
+ $renderer = Mockery::mock(DefaultRenderer::class)->makePartial();
+ $renderer->shouldReceive('getBaseUrl')->andReturn('https://magento2.test/');
+
+ $this->renderer = $renderer;
+ }
+
+ /**
+ * @param string $url
+ * @return Item|MockInterface
+ */
+ private function mockItem(string $url): Item|MockInterface
+ {
+ $item = Mockery::mock(Item::class);
+ $item->shouldReceive('getUrl')->andReturn($url);
+
+ return $item;
+ }
+}
diff --git a/tests/Unit/Model/Catalog/Layer/UrlTest.php b/tests/Unit/Model/Catalog/Layer/UrlTest.php
new file mode 100644
index 00000000..7a37ba52
--- /dev/null
+++ b/tests/Unit/Model/Catalog/Layer/UrlTest.php
@@ -0,0 +1,220 @@
+urlStrategyFactory = Mockery::mock(UrlStrategyFactory::class);
+ $this->request = Mockery::mock(MagentoHttpRequest::class);
+ $this->config = Mockery::mock(Config::class);
+ $this->currentContext = Mockery::mock(CurrentContext::class);
+ $this->categoryUrlStrategy = Mockery::mock(CategoryUrlInterface::class);
+
+ $this->subject = new Url(
+ $this->urlStrategyFactory,
+ $this->request,
+ Mockery::mock(CategoryRepositoryInterface::class),
+ Mockery::mock(ExportHelper::class),
+ Mockery::mock(UrlModel::class),
+ $this->config,
+ $this->currentContext,
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetSelectFilterReturnsTweakwiseCategoryLinkOnCategoryPageWhenEnabled(): void
+ {
+ $item = $this->createCategoryItem('https://magento2.test/default/women/tops-women2/');
+ $this->config->shouldReceive('isCategoryUrlFromTweakwiseEnabled')->andReturn(true);
+ $this->currentContext->shouldReceive('getRequest')->andReturn(Mockery::mock(ProductNavigationRequest::class));
+ $this->urlStrategyFactory->shouldNotReceive('create');
+
+ $result = $this->subject->getSelectFilter($item);
+
+ $this->assertEquals('https://magento2.test/default/women/tops-women2/', $result);
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetSelectFilterFallsBackToMagentoCategoryUrlWhenSettingIsDisabled(): void
+ {
+ $item = $this->createCategoryItem('https://magento2.test/default/women/tops-women2/');
+ $this->config->shouldReceive('isCategoryUrlFromTweakwiseEnabled')->andReturn(false);
+ $this->currentContext->shouldNotReceive('getRequest');
+ $this->urlStrategyFactory
+ ->shouldReceive('create')
+ ->with(CategoryUrlInterface::class)
+ ->once()
+ ->andReturn($this->categoryUrlStrategy);
+ $this->categoryUrlStrategy
+ ->shouldReceive('getCategoryFilterSelectUrl')
+ ->with($this->request, $item)
+ ->once()
+ ->andReturn('/default/women/tops-women2/facet/');
+
+ $result = $this->subject->getSelectFilter($item);
+
+ $this->assertEquals('/default/women/tops-women2/facet/', $result);
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetSelectFilterFallsBackToMagentoCategoryUrlWhenLinkIsEmpty(): void
+ {
+ $item = $this->createCategoryItem('');
+ $this->config->shouldReceive('isCategoryUrlFromTweakwiseEnabled')->andReturn(true);
+ $this->currentContext->shouldNotReceive('getRequest');
+ $this->urlStrategyFactory
+ ->shouldReceive('create')
+ ->with(CategoryUrlInterface::class)
+ ->once()
+ ->andReturn($this->categoryUrlStrategy);
+ $this->categoryUrlStrategy
+ ->shouldReceive('getCategoryFilterSelectUrl')
+ ->with($this->request, $item)
+ ->once()
+ ->andReturn('/default/women/tops-women2/facet/');
+
+ $result = $this->subject->getSelectFilter($item);
+
+ $this->assertEquals('/default/women/tops-women2/facet/', $result);
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetSelectFilterFallsBackToMagentoCategoryUrlOnSearchRequest(): void
+ {
+ $item = $this->createCategoryItem('https://magento2.test/default/women/tops-women2/');
+ $this->config->shouldReceive('isCategoryUrlFromTweakwiseEnabled')->andReturn(true);
+ $this->currentContext->shouldReceive('getRequest')->andReturn(Mockery::mock(ProductSearchRequest::class));
+ $this->urlStrategyFactory
+ ->shouldReceive('create')
+ ->with(CategoryUrlInterface::class)
+ ->once()
+ ->andReturn($this->categoryUrlStrategy);
+ $this->categoryUrlStrategy
+ ->shouldReceive('getCategoryFilterSelectUrl')
+ ->with($this->request, $item)
+ ->once()
+ ->andReturn('/catalogsearch/result/?cat=tops-women2');
+
+ $result = $this->subject->getSelectFilter($item);
+
+ $this->assertEquals('/catalogsearch/result/?cat=tops-women2', $result);
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetSelectFilterFallsBackToMagentoCategoryUrlWhenContextIsUnavailable(): void
+ {
+ $item = $this->createCategoryItem('https://magento2.test/default/women/tops-women2/');
+ $this->config->shouldReceive('isCategoryUrlFromTweakwiseEnabled')->andReturn(true);
+ $this->currentContext->shouldReceive('getRequest')->andThrow(new Exception('No active context'));
+ $this->urlStrategyFactory
+ ->shouldReceive('create')
+ ->with(CategoryUrlInterface::class)
+ ->once()
+ ->andReturn($this->categoryUrlStrategy);
+ $this->categoryUrlStrategy
+ ->shouldReceive('getCategoryFilterSelectUrl')
+ ->with($this->request, $item)
+ ->once()
+ ->andReturn('/default/women/tops-women2/facet/');
+
+ $result = $this->subject->getSelectFilter($item);
+
+ $this->assertEquals('/default/women/tops-women2/facet/', $result);
+ }
+
+ /**
+ * @param string $link
+ * @return Item|MockInterface
+ */
+ private function createCategoryItem(string $link): Item|MockInterface
+ {
+ $settings = new SettingsType(['source' => SettingsType::SOURCE_CATEGORY]);
+
+ $facetType = new FacetType();
+ $facetType->setFacetSettings($settings);
+
+ $filter = Mockery::mock(Filter::class);
+ $filter->shouldReceive('getFacet')->andReturn($facetType);
+
+ $attribute = Mockery::mock(AttributeType::class);
+ $attribute->shouldReceive('getLink')->andReturn($link);
+
+ $item = Mockery::mock(Item::class);
+ $item->shouldReceive('getFilter')->andReturn($filter);
+ $item->shouldReceive('getAttribute')->andReturn($attribute);
+
+ return $item;
+ }
+}
diff --git a/tests/Unit/Model/Client/Type/AttributeTypeTest.php b/tests/Unit/Model/Client/Type/AttributeTypeTest.php
new file mode 100644
index 00000000..c7992bb3
--- /dev/null
+++ b/tests/Unit/Model/Client/Type/AttributeTypeTest.php
@@ -0,0 +1,31 @@
+ 'https://magento2.test/default/women/tops-women2/']);
+
+ $this->assertSame('https://magento2.test/default/women/tops-women2/', $attribute->getLink());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetLinkReturnsEmptyStringWhenLinkIsMissing(): void
+ {
+ $attribute = new AttributeType();
+
+ $this->assertSame('', $attribute->getLink());
+ }
+}