= $block->escapeHtml(
@@ -12,7 +15,8 @@
); ?>
= $block->escapeHtml(
- __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.')
+ __('Select values from each attribute to include in this product. ' .
+ 'Each unique combination of values creates a unique product SKU.')
);?>
@@ -72,7 +76,8 @@
-
@@ -120,8 +125,8 @@
"= /* @noEscape */ $block->getComponentName() ?>": {
"component": "Magento_ConfigurableProduct/js/variations/steps/attributes_values",
"appendTo": "= /* @noEscape */ $block->getParentComponentName() ?>",
- "optionsUrl": "= /* @noEscape */ $block->getUrl('catalog/product_attribute/getAttributes') ?>",
- "createOptionsUrl": "= /* @noEscape */ $block->getUrl('catalog/product_attribute/createOptions') ?>"
+ "optionsUrl": "= /* @noEscape */ $attributesUrl ?>",
+ "createOptionsUrl": "= /* @noEscape */ $optionsUrl ?>"
}
}
}
diff --git a/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php
new file mode 100644
index 0000000000000..c2b7189b808a3
--- /dev/null
+++ b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php
@@ -0,0 +1,78 @@
+orderRepository = $orderRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ }
+
+ /**
+ * Upgrade order customer email when customer has changed email
+ *
+ * @param Observer $observer
+ * @return void
+ */
+ public function execute(Observer $observer): void
+ {
+ /** @var Customer $originalCustomer */
+ $originalCustomer = $observer->getEvent()->getOrigCustomerDataObject();
+ if (!$originalCustomer) {
+ return;
+ }
+
+ /** @var Customer $customer */
+ $customer = $observer->getEvent()->getCustomerDataObject();
+ $customerEmail = $customer->getEmail();
+
+ if ($customerEmail === $originalCustomer->getEmail()) {
+ return;
+ }
+ $searchCriteria = $this->searchCriteriaBuilder
+ ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId())
+ ->create();
+
+ /**
+ * @var Collection $orders
+ */
+ $orders = $this->orderRepository->getList($searchCriteria);
+ $orders->setDataToAll(OrderInterface::CUSTOMER_EMAIL, $customerEmail);
+ $orders->save();
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml
new file mode 100644
index 0000000000000..34d01d09b42cf
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ Create Order via API assigned to Customer.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
index ec5141d84b1bd..61ce050aa3ef2 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml
@@ -17,5 +17,9 @@
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
new file mode 100644
index 0000000000000..ba113c739d706
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php
new file mode 100644
index 0000000000000..d05c10c00e6c3
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php
@@ -0,0 +1,222 @@
+orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class)
+ ->getMock();
+
+ $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->eventMock = $this->getMockBuilder(Event::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getCustomerDataObject', 'getOrigCustomerDataObject'])
+ ->getMock();
+
+ $this->observerMock = $this->getMockBuilder(Observer::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->observerMock->expects($this->any())->method('getEvent')->willReturn($this->eventMock);
+
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+
+ $this->orderCustomerEmailObserver = $this->objectManagerHelper->getObject(
+ UpgradeOrderCustomerEmailObserver::class,
+ [
+ 'orderRepository' => $this->orderRepositoryMock,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
+ ]
+ );
+ }
+
+ /**
+ * Verifying that the order email is not updated when the customer email is not updated
+ *
+ */
+ public function testUpgradeOrderCustomerEmailWhenMailIsNotChanged(): void
+ {
+ $customer = $this->createCustomerMock();
+ $originalCustomer = $this->createCustomerMock();
+
+ $this->setCustomerToEventMock($customer);
+ $this->setOriginalCustomerToEventMock($originalCustomer);
+
+ $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL);
+ $this->setCustomerEmail($customer, self::ORIGINAL_CUSTOMER_EMAIL);
+
+ $this->whenOrderRepositoryGetListIsNotCalled();
+
+ $this->orderCustomerEmailObserver->execute($this->observerMock);
+ }
+
+ /**
+ * Verifying that the order email is updated after the customer updates their email
+ *
+ */
+ public function testUpgradeOrderCustomerEmail(): void
+ {
+ $customer = $this->createCustomerMock();
+ $originalCustomer = $this->createCustomerMock();
+ $orderCollectionMock = $this->createOrderMock();
+
+ $this->setCustomerToEventMock($customer);
+ $this->setOriginalCustomerToEventMock($originalCustomer);
+
+ $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL);
+ $this->setCustomerEmail($customer, self::NEW_CUSTOMER_EMAIL);
+
+ $this->whenOrderRepositoryGetListIsCalled($orderCollectionMock);
+
+ $this->whenOrderCollectionSetDataToAllIsCalled($orderCollectionMock);
+
+ $this->whenOrderCollectionSaveIsCalled($orderCollectionMock);
+
+ $this->orderCustomerEmailObserver->execute($this->observerMock);
+ }
+
+ private function createCustomerMock(): MockObject
+ {
+ $customer = $this->getMockBuilder(CustomerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $customer;
+ }
+
+ private function createOrderMock(): MockObject
+ {
+ $orderCollectionMock = $this->getMockBuilder(Collection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $orderCollectionMock;
+ }
+
+ private function setCustomerToEventMock(MockObject $customer): void
+ {
+ $this->eventMock->expects($this->once())
+ ->method('getCustomerDataObject')
+ ->willReturn($customer);
+ }
+
+ private function setOriginalCustomerToEventMock(MockObject $originalCustomer): void
+ {
+ $this->eventMock->expects($this->once())
+ ->method('getOrigCustomerDataObject')
+ ->willReturn($originalCustomer);
+ }
+
+ private function setCustomerEmail(MockObject $originalCustomer, string $email): void
+ {
+ $originalCustomer->expects($this->once())
+ ->method('getEmail')
+ ->willReturn($email);
+ }
+
+ private function whenOrderRepositoryGetListIsCalled(MockObject $orderCollectionMock): void
+ {
+ $searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('create')
+ ->willReturn($searchCriteriaMock);
+
+ $this->searchCriteriaBuilderMock->expects($this->once())
+ ->method('addFilter')
+ ->willReturn($this->searchCriteriaBuilderMock);
+
+ $this->orderRepositoryMock->expects($this->once())
+ ->method('getList')
+ ->with($searchCriteriaMock)
+ ->willReturn($orderCollectionMock);
+ }
+
+ private function whenOrderCollectionSetDataToAllIsCalled(MockObject $orderCollectionMock): void
+ {
+ $orderCollectionMock->expects($this->once())
+ ->method('setDataToAll')
+ ->with(OrderInterface::CUSTOMER_EMAIL, self::NEW_CUSTOMER_EMAIL);
+ }
+
+ private function whenOrderCollectionSaveIsCalled(MockObject $orderCollectionMock): void
+ {
+ $orderCollectionMock->expects($this->once())
+ ->method('save');
+ }
+
+ private function whenOrderRepositoryGetListIsNotCalled(): void
+ {
+ $this->searchCriteriaBuilderMock->expects($this->never())
+ ->method('addFilter');
+ $this->searchCriteriaBuilderMock->expects($this->never())
+ ->method('create');
+
+ $this->orderRepositoryMock->expects($this->never())
+ ->method('getList');
+ }
+}
diff --git a/app/code/Magento/Customer/etc/events.xml b/app/code/Magento/Customer/etc/events.xml
index 2a724498a0359..0194f91c591f5 100644
--- a/app/code/Magento/Customer/etc/events.xml
+++ b/app/code/Magento/Customer/etc/events.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
index c0bc825a8285b..c449f8f54872f 100644
--- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
+++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php
@@ -7,8 +7,9 @@
namespace Magento\Downloadable\Controller\Download;
-use Magento\Catalog\Model\Product\SalabilityChecker;
use Magento\Downloadable\Helper\Download as DownloadHelper;
+use Magento\Downloadable\Model\Link as LinkModel;
+use Magento\Downloadable\Model\RelatedProductRetriever;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
@@ -20,20 +21,21 @@
class LinkSample extends \Magento\Downloadable\Controller\Download
{
/**
- * @var SalabilityChecker
+ * @var RelatedProductRetriever
*/
- private $salabilityChecker;
+ private $relatedProductRetriever;
/**
* @param Context $context
- * @param SalabilityChecker|null $salabilityChecker
+ * @param RelatedProductRetriever $relatedProductRetriever
*/
public function __construct(
Context $context,
- SalabilityChecker $salabilityChecker = null
+ RelatedProductRetriever $relatedProductRetriever
) {
parent::__construct($context);
- $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class);
+
+ $this->relatedProductRetriever = $relatedProductRetriever;
}
/**
@@ -44,9 +46,10 @@ public function __construct(
public function execute()
{
$linkId = $this->getRequest()->getParam('link_id', 0);
- /** @var \Magento\Downloadable\Model\Link $link */
- $link = $this->_objectManager->create(\Magento\Downloadable\Model\Link::class)->load($linkId);
- if ($link->getId() && $this->salabilityChecker->isSalable($link->getProductId())) {
+ /** @var LinkModel $link */
+ $link = $this->_objectManager->create(LinkModel::class);
+ $link->load($linkId);
+ if ($link->getId() && $this->isProductSalable($link)) {
$resource = '';
$resourceType = '';
if ($link->getSampleType() == DownloadHelper::LINK_TYPE_URL) {
@@ -74,4 +77,16 @@ public function execute()
return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl());
}
+
+ /**
+ * Check is related product salable.
+ *
+ * @param LinkModel $link
+ * @return bool
+ */
+ private function isProductSalable(LinkModel $link): bool
+ {
+ $product = $this->relatedProductRetriever->getProduct((int) $link->getProductId());
+ return $product ? $product->isSalable() : false;
+ }
}
diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php
index b95ec510fdd9b..e2561092a7592 100644
--- a/app/code/Magento/Downloadable/Controller/Download/Sample.php
+++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php
@@ -7,8 +7,9 @@
namespace Magento\Downloadable\Controller\Download;
-use Magento\Catalog\Model\Product\SalabilityChecker;
use Magento\Downloadable\Helper\Download as DownloadHelper;
+use Magento\Downloadable\Model\RelatedProductRetriever;
+use Magento\Downloadable\Model\Sample as SampleModel;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
@@ -20,20 +21,21 @@
class Sample extends \Magento\Downloadable\Controller\Download
{
/**
- * @var SalabilityChecker
+ * @var RelatedProductRetriever
*/
- private $salabilityChecker;
+ private $relatedProductRetriever;
/**
* @param Context $context
- * @param SalabilityChecker|null $salabilityChecker
+ * @param RelatedProductRetriever $relatedProductRetriever
*/
public function __construct(
Context $context,
- SalabilityChecker $salabilityChecker = null
+ RelatedProductRetriever $relatedProductRetriever
) {
parent::__construct($context);
- $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class);
+
+ $this->relatedProductRetriever = $relatedProductRetriever;
}
/**
@@ -44,9 +46,10 @@ public function __construct(
public function execute()
{
$sampleId = $this->getRequest()->getParam('sample_id', 0);
- /** @var \Magento\Downloadable\Model\Sample $sample */
- $sample = $this->_objectManager->create(\Magento\Downloadable\Model\Sample::class)->load($sampleId);
- if ($sample->getId() && $this->salabilityChecker->isSalable($sample->getProductId())) {
+ /** @var SampleModel $sample */
+ $sample = $this->_objectManager->create(SampleModel::class);
+ $sample->load($sampleId);
+ if ($sample->getId() && $this->isProductSalable($sample)) {
$resource = '';
$resourceType = '';
if ($sample->getSampleType() == DownloadHelper::LINK_TYPE_URL) {
@@ -71,4 +74,16 @@ public function execute()
return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl());
}
+
+ /**
+ * Check is related product salable.
+ *
+ * @param SampleModel $sample
+ * @return bool
+ */
+ private function isProductSalable(SampleModel $sample): bool
+ {
+ $product = $this->relatedProductRetriever->getProduct((int) $sample->getProductId());
+ return $product ? $product->isSalable() : false;
+ }
}
diff --git a/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php
new file mode 100644
index 0000000000000..f701f96b910e7
--- /dev/null
+++ b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php
@@ -0,0 +1,68 @@
+productRepository = $productRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->metadataPool = $metadataPool;
+ }
+
+ /**
+ * Get related product.
+ *
+ * @param int $productId
+ * @return ProductInterface|null
+ */
+ public function getProduct(int $productId): ?ProductInterface
+ {
+ $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class);
+
+ $searchCriteria = $this->searchCriteriaBuilder->addFilter($productMetadata->getLinkField(), $productId)
+ ->create();
+ $items = $this->productRepository->getList($searchCriteria)
+ ->getItems();
+ $product = $items ? array_shift($items) : null;
+
+ return $product;
+ }
+}
diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
index 8d30322745b8d..b7b079d208d97 100644
--- a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
+++ b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php
@@ -24,7 +24,7 @@ class Sample extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
/**
* @param \Magento\Framework\Model\ResourceModel\Db\Context $context
* @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
- * @param null $connectionName
+ * @param string|null $connectionName
*/
public function __construct(
\Magento\Framework\Model\ResourceModel\Db\Context $context,
@@ -126,7 +126,7 @@ public function getSearchableData($productId, $storeId)
)->join(
['cpe' => $this->getTable('catalog_product_entity')],
sprintf(
- 'cpe.entity_id = m.product_id',
+ 'cpe.%s = m.product_id',
$this->metadataPool->getMetadata(ProductInterface::class)->getLinkField()
),
[]
diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php
deleted file mode 100644
index 725c06004f117..0000000000000
--- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php
+++ /dev/null
@@ -1,237 +0,0 @@
-objectManagerHelper = new ObjectManagerHelper($this);
-
- $this->request = $this->getMockForAbstractClass(RequestInterface::class);
- $this->response = $this->getMockBuilder(ResponseInterface::class)
- ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect'])
- ->onlyMethods(['sendResponse'])
- ->getMockForAbstractClass();
-
- $this->helperData = $this->createPartialMock(
- Data::class,
- ['getIsShareable']
- );
- $this->downloadHelper = $this->createPartialMock(
- Download::class,
- [
- 'setResource',
- 'getFilename',
- 'getContentType',
- 'getFileSize',
- 'getContentDisposition',
- 'output'
- ]
- );
- $this->product = $this->getMockBuilder(Product::class)
- ->addMethods(['_wakeup'])
- ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class);
- $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class);
- $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class);
- $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class);
- $this->objectManager = $this->createPartialMock(
- \Magento\Framework\ObjectManager\ObjectManager::class,
- ['create', 'get']
- );
- $this->linkSample = $this->objectManagerHelper->getObject(
- LinkSample::class,
- [
- 'objectManager' => $this->objectManager,
- 'request' => $this->request,
- 'response' => $this->response,
- 'messageManager' => $this->messageManager,
- 'redirect' => $this->redirect,
- 'salabilityChecker' => $this->salabilityCheckerMock,
- ]
- );
- }
-
- /**
- * Execute Download link's sample action with Url link.
- *
- * @return void
- */
- public function testExecuteLinkTypeUrl()
- {
- $linkMock = $this->getMockBuilder(Link::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id');
- $this->objectManager->expects($this->once())
- ->method('create')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf();
- $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $linkMock->expects($this->once())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_URL
- );
- $linkMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url');
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->linkSample->execute());
- }
-
- /**
- * Execute Download link's sample action with File link.
- *
- * @return void
- */
- public function testExecuteLinkTypeFile()
- {
- $linkMock = $this->getMockBuilder(Link::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath'])
- ->getMock();
- $fileMock = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->setMethods(['getFilePath', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id');
- $this->objectManager->expects($this->at(0))
- ->method('create')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('load')->with('some_link_id')->willReturnSelf();
- $linkMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $linkMock->expects($this->any())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_FILE
- );
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(File::class)
- ->willReturn($fileMock);
- $this->objectManager->expects($this->at(2))
- ->method('get')
- ->with(Link::class)
- ->willReturn($linkMock);
- $linkMock->expects($this->once())->method('getBaseSamplePath')->willReturn('downloadable/files/link_samples');
- $this->objectManager->expects($this->at(3))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->linkSample->execute());
- }
-}
diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php
deleted file mode 100644
index 6dcd09a91dd2e..0000000000000
--- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php
+++ /dev/null
@@ -1,232 +0,0 @@
-objectManagerHelper = new ObjectManagerHelper($this);
-
- $this->request = $this->getMockForAbstractClass(RequestInterface::class);
- $this->response = $this->getMockBuilder(ResponseInterface::class)
- ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect'])
- ->onlyMethods(['sendResponse'])
- ->getMockForAbstractClass();
-
- $this->helperData = $this->createPartialMock(
- Data::class,
- ['getIsShareable']
- );
- $this->downloadHelper = $this->createPartialMock(
- Download::class,
- [
- 'setResource',
- 'getFilename',
- 'getContentType',
- 'getFileSize',
- 'getContentDisposition',
- 'output'
- ]
- );
- $this->product = $this->getMockBuilder(Product::class)
- ->addMethods(['_wakeup'])
- ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class);
- $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class);
- $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class);
- $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class);
- $this->objectManager = $this->createPartialMock(
- \Magento\Framework\ObjectManager\ObjectManager::class,
- ['create', 'get']
- );
- $this->sample = $this->objectManagerHelper->getObject(
- Sample::class,
- [
- 'objectManager' => $this->objectManager,
- 'request' => $this->request,
- 'response' => $this->response,
- 'messageManager' => $this->messageManager,
- 'redirect' => $this->redirect,
- 'salabilityChecker' => $this->salabilityCheckerMock,
- ]
- );
- }
-
- /**
- * Execute Download sample action with Sample Url.
- *
- * @return void
- */
- public function testExecuteSampleWithUrlType()
- {
- $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id');
- $this->objectManager->expects($this->once())
- ->method('create')
- ->with(\Magento\Downloadable\Model\Sample::class)
- ->willReturn($sampleMock);
- $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf();
- $sampleMock->expects($this->once())->method('getId')->willReturn('some_link_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $sampleMock->expects($this->once())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_URL
- );
- $sampleMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url');
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->sample->execute());
- }
-
- /**
- * Execute Download sample action with Sample File.
- *
- * @return void
- */
- public function testExecuteSampleWithFileType()
- {
- $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class)
- ->disableOriginalConstructor()
- ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath'])
- ->getMock();
- $fileHelperMock = $this->getMockBuilder(File::class)
- ->disableOriginalConstructor()
- ->setMethods(['getFilePath'])
- ->getMock();
-
- $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id');
- $this->objectManager->expects($this->at(0))
- ->method('create')
- ->with(\Magento\Downloadable\Model\Sample::class)
- ->willReturn($sampleMock);
- $sampleMock->expects($this->once())->method('load')->with('some_sample_id')->willReturnSelf();
- $sampleMock->expects($this->once())->method('getId')->willReturn('some_sample_id');
- $this->salabilityCheckerMock->expects($this->once())->method('isSalable')->willReturn(true);
- $sampleMock->expects($this->any())->method('getSampleType')->willReturn(
- Download::LINK_TYPE_FILE
- );
- $this->objectManager->expects($this->at(1))
- ->method('get')
- ->with(File::class)
- ->willReturn($fileHelperMock);
- $fileHelperMock->expects($this->once())->method('getFilePath')->willReturn('file_path');
- $this->objectManager->expects($this->at(2))
- ->method('get')
- ->with(Download::class)
- ->willReturn($this->downloadHelper);
- $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf();
- $this->response->expects($this->any())->method('setHeader')->willReturnSelf();
- $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception());
- $this->messageManager->expects($this->once())
- ->method('addError')
- ->with('Sorry, there was an error getting requested content. Please contact the store owner.')
- ->willReturnSelf();
- $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url');
- $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf();
-
- $this->assertEquals($this->response, $this->sample->execute());
- }
-}
diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php
index 39148f990b714..4366ef7aaf969 100644
--- a/app/code/Magento/Quote/Model/Quote/Address.php
+++ b/app/code/Magento/Quote/Model/Quote/Address.php
@@ -1019,6 +1019,13 @@ public function collectShippingRates()
*/
public function requestShippingRates(AbstractItem $item = null)
{
+ $storeId = $this->getQuote()->getStoreId() ?: $this->storeManager->getStore()->getId();
+ $taxInclude = $this->_scopeConfig->getValue(
+ 'tax/calculation/price_includes_tax',
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+
/** @var $request RateRequest */
$request = $this->_rateRequestFactory->create();
$request->setAllItems($item ? [$item] : $this->getAllItems());
@@ -1028,9 +1035,11 @@ public function requestShippingRates(AbstractItem $item = null)
$request->setDestStreet($this->getStreetFull());
$request->setDestCity($this->getCity());
$request->setDestPostcode($this->getPostcode());
- $request->setPackageValue($item ? $item->getBaseRowTotal() : $this->getBaseSubtotal());
+ $baseSubtotal = $taxInclude ? $this->getBaseSubtotalTotalInclTax() : $this->getBaseSubtotal();
+ $request->setPackageValue($item ? $item->getBaseRowTotal() : $baseSubtotal);
+ $baseSubtotalWithDiscount = $baseSubtotal + $this->getBaseDiscountAmount();
$packageWithDiscount = $item ? $item->getBaseRowTotal() -
- $item->getBaseDiscountAmount() : $this->getBaseSubtotalWithDiscount();
+ $item->getBaseDiscountAmount() : $baseSubtotalWithDiscount;
$request->setPackageValueWithDiscount($packageWithDiscount);
$request->setPackageWeight($item ? $item->getRowWeight() : $this->getWeight());
$request->setPackageQty($item ? $item->getQty() : $this->getItemQty());
@@ -1038,8 +1047,7 @@ public function requestShippingRates(AbstractItem $item = null)
/**
* Need for shipping methods that use insurance based on price of physical products
*/
- $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $this->getBaseSubtotal() -
- $this->getBaseVirtualAmount();
+ $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $baseSubtotal - $this->getBaseVirtualAmount();
$request->setPackagePhysicalValue($packagePhysicalValue);
$request->setFreeMethodWeight($item ? 0 : $this->getFreeMethodWeight());
@@ -1047,12 +1055,10 @@ public function requestShippingRates(AbstractItem $item = null)
/**
* Store and website identifiers specified from StoreManager
*/
+ $request->setStoreId($storeId);
if ($this->getQuote()->getStoreId()) {
- $storeId = $this->getQuote()->getStoreId();
- $request->setStoreId($storeId);
$request->setWebsiteId($this->storeManager->getStore($storeId)->getWebsiteId());
} else {
- $request->setStoreId($this->storeManager->getStore()->getId());
$request->setWebsiteId($this->storeManager->getWebsite()->getId());
}
$request->setFreeShipping($this->getFreeShipping());
diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml
new file mode 100755
index 0000000000000..a14be3b533fa8
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+ ShippingAddressTX
+ BillingAddressTX
+ flatrate
+ flatrate
+
+
+
+
+ PaymentMethodCheckMoneyOrder
+ BillingAddressTX
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml
new file mode 100644
index 0000000000000..3681245311188
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ 1
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml
new file mode 100644
index 0000000000000..f5555394f8d4d
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ application/json
+
+ string
+ string
+ integer
+
+
+
diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml
new file mode 100644
index 0000000000000..f233954f2cdcf
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+ application/json
+ string
+
+
+
+ application/json
+ string
+
+
+ string
+ string
+ string
+ integer
+ string
+
+ string
+
+ string
+ string
+ string
+ string
+ string
+
+
+ string
+ string
+ string
+ integer
+ string
+
+ string
+
+ string
+ string
+ string
+ string
+ string
+
+ string
+ string
+
+
+
+
+ application/json
+ string
+
+ string
+
+
+
diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
index a8fd794c08757..d4f6778a2ccb8 100644
--- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php
@@ -352,10 +352,40 @@ public function testRequestShippingRates()
$currentCurrencyCode = 'UAH';
+ $this->quote->expects($this->any())
+ ->method('getStoreId')
+ ->willReturn($storeId);
+
+ $this->storeManager->expects($this->at(0))
+ ->method('getStore')
+ ->with($storeId)
+ ->willReturn($this->store);
+ $this->store->expects($this->any())
+ ->method('getWebsiteId')
+ ->willReturn($webSiteId);
+
+ $this->scopeConfig->expects($this->exactly(1))
+ ->method('getValue')
+ ->with(
+ 'tax/calculation/price_includes_tax',
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ )
+ ->willReturn(1);
+
/** @var RateRequest */
$request = $this->getMockBuilder(RateRequest::class)
->disableOriginalConstructor()
- ->setMethods(['setStoreId', 'setWebsiteId', 'setBaseCurrency', 'setPackageCurrency'])
+ ->setMethods(
+ [
+ 'setStoreId',
+ 'setWebsiteId',
+ 'setBaseCurrency',
+ 'setPackageCurrency',
+ 'getBaseSubtotalTotalInclTax',
+ 'getBaseSubtotal'
+ ]
+ )
->getMock();
/** @var Collection */
@@ -434,13 +464,6 @@ public function testRequestShippingRates()
$this->storeManager->method('getStore')
->willReturn($this->store);
- $this->storeManager->expects($this->once())
- ->method('getWebsite')
- ->willReturn($this->website);
-
- $this->store->method('getId')
- ->willReturn($storeId);
-
$this->store->method('getBaseCurrency')
->willReturn($baseCurrency);
@@ -452,10 +475,6 @@ public function testRequestShippingRates()
->method('getCurrentCurrencyCode')
->willReturn($currentCurrencyCode);
- $this->website->expects($this->once())
- ->method('getId')
- ->willReturn($webSiteId);
-
$this->addressRateFactory->expects($this->once())
->method('create')
->willReturn($rate);
diff --git a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
index dc3a8e9f69aca..0529c66a04d8c 100644
--- a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
+++ b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html
@@ -8,7 +8,7 @@
+
+
+
+
+ Switch the Storefront to the provided Store.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php
index fc13372520945..9ba1083adab74 100644
--- a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php
+++ b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php
@@ -5,11 +5,17 @@
*/
namespace Magento\Swatches\Block\LayeredNavigation;
-use Magento\Eav\Model\Entity\Attribute;
+use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
+use Magento\Catalog\Model\Layer\Filter\Item as FilterItem;
use Magento\Catalog\Model\ResourceModel\Layer\Filter\AttributeFactory;
-use Magento\Framework\View\Element\Template;
+use Magento\Eav\Model\Entity\Attribute;
use Magento\Eav\Model\Entity\Attribute\Option;
-use Magento\Catalog\Model\Layer\Filter\Item as FilterItem;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\View\Element\Template;
+use Magento\Framework\View\Element\Template\Context;
+use Magento\Swatches\Helper\Data;
+use Magento\Swatches\Helper\Media;
+use Magento\Theme\Block\Html\Pager;
/**
* Class RenderLayered Render Swatches at Layered Navigation
@@ -37,7 +43,7 @@ class RenderLayered extends Template
protected $eavAttribute;
/**
- * @var \Magento\Catalog\Model\Layer\Filter\AbstractFilter
+ * @var AbstractFilter
*/
protected $filter;
@@ -47,41 +53,52 @@ class RenderLayered extends Template
protected $layerAttribute;
/**
- * @var \Magento\Swatches\Helper\Data
+ * @var Data
*/
protected $swatchHelper;
/**
- * @var \Magento\Swatches\Helper\Media
+ * @var Media
*/
protected $mediaHelper;
/**
- * @param Template\Context $context
+ * @var Pager
+ */
+ private $htmlPagerBlock;
+
+ /**
+ * @param Context $context
* @param Attribute $eavAttribute
* @param AttributeFactory $layerAttribute
- * @param \Magento\Swatches\Helper\Data $swatchHelper
- * @param \Magento\Swatches\Helper\Media $mediaHelper
+ * @param Data $swatchHelper
+ * @param Media $mediaHelper
* @param array $data
+ * @param Pager|null $htmlPagerBlock
*/
public function __construct(
- \Magento\Framework\View\Element\Template\Context $context,
+ Context $context,
Attribute $eavAttribute,
AttributeFactory $layerAttribute,
- \Magento\Swatches\Helper\Data $swatchHelper,
- \Magento\Swatches\Helper\Media $mediaHelper,
- array $data = []
+ Data $swatchHelper,
+ Media $mediaHelper,
+ array $data = [],
+ ?Pager $htmlPagerBlock = null
) {
$this->eavAttribute = $eavAttribute;
$this->layerAttribute = $layerAttribute;
$this->swatchHelper = $swatchHelper;
$this->mediaHelper = $mediaHelper;
+ $this->htmlPagerBlock = $htmlPagerBlock ?? ObjectManager::getInstance()->get(Pager::class);
parent::__construct($context, $data);
}
/**
+ * Set filter and attribute objects
+ *
* @param \Magento\Catalog\Model\Layer\Filter\AbstractFilter $filter
+ *
* @return $this
* @throws \Magento\Framework\Exception\LocalizedException
*/
@@ -94,6 +111,8 @@ public function setSwatchFilter(\Magento\Catalog\Model\Layer\Filter\AbstractFilt
}
/**
+ * Get attribute swatch data
+ *
* @return array
*/
public function getSwatchData()
@@ -114,30 +133,46 @@ public function getSwatchData()
$attributeOptionIds = array_keys($attributeOptions);
$swatches = $this->swatchHelper->getSwatchesByOptionsId($attributeOptionIds);
- $data = [
+ return [
'attribute_id' => $this->eavAttribute->getId(),
'attribute_code' => $this->eavAttribute->getAttributeCode(),
'attribute_label' => $this->eavAttribute->getStoreLabel(),
'options' => $attributeOptions,
'swatches' => $swatches,
];
-
- return $data;
}
/**
+ * Build filter option url
+ *
* @param string $attributeCode
* @param int $optionId
+ *
* @return string
*/
public function buildUrl($attributeCode, $optionId)
{
- $query = [$attributeCode => $optionId];
- return $this->_urlBuilder->getUrl('*/*/*', ['_current' => true, '_use_rewrite' => true, '_query' => $query]);
+ $query = [
+ $attributeCode => $optionId,
+ // exclude current page from urls
+ $this->htmlPagerBlock->getPageVarName() => null
+ ];
+
+ return $this->_urlBuilder->getUrl(
+ '*/*/*',
+ [
+ '_current' => true,
+ '_use_rewrite' => true,
+ '_query' => $query
+ ]
+ );
}
/**
+ * Get view data for option with no results
+ *
* @param Option $swatchOption
+ *
* @return array
*/
protected function getUnusedOption(Option $swatchOption)
@@ -150,8 +185,11 @@ protected function getUnusedOption(Option $swatchOption)
}
/**
+ * Get option data if visible
+ *
* @param FilterItem[] $filterItems
* @param Option $swatchOption
+ *
* @return array
*/
protected function getFilterOption(array $filterItems, Option $swatchOption)
@@ -166,8 +204,11 @@ protected function getFilterOption(array $filterItems, Option $swatchOption)
}
/**
+ * Get view data for option
+ *
* @param FilterItem $filterItem
* @param Option $swatchOption
+ *
* @return array
*/
protected function getOptionViewData(FilterItem $filterItem, Option $swatchOption)
@@ -187,15 +228,20 @@ protected function getOptionViewData(FilterItem $filterItem, Option $swatchOptio
}
/**
+ * Check if option should be visible
+ *
* @param FilterItem $filterItem
+ *
* @return bool
*/
protected function isOptionVisible(FilterItem $filterItem)
{
- return $this->isOptionDisabled($filterItem) && $this->isShowEmptyResults() ? false : true;
+ return !($this->isOptionDisabled($filterItem) && $this->isShowEmptyResults());
}
/**
+ * Check if attribute values should be visible with no results
+ *
* @return bool
*/
protected function isShowEmptyResults()
@@ -204,7 +250,10 @@ protected function isShowEmptyResults()
}
/**
+ * Check if option should be disabled
+ *
* @param FilterItem $filterItem
+ *
* @return bool
*/
protected function isOptionDisabled(FilterItem $filterItem)
@@ -213,8 +262,11 @@ protected function isOptionDisabled(FilterItem $filterItem)
}
/**
+ * Retrieve filter item by id
+ *
* @param FilterItem[] $filterItems
* @param integer $id
+ *
* @return bool|FilterItem
*/
protected function getFilterItemById(array $filterItems, $id)
@@ -228,14 +280,15 @@ protected function getFilterItemById(array $filterItems, $id)
}
/**
+ * Get swatch image path
+ *
* @param string $type
* @param string $filename
+ *
* @return string
*/
public function getSwatchPath($type, $filename)
{
- $imagePath = $this->mediaHelper->getSwatchAttributeImage($type, $filename);
-
- return $imagePath;
+ return $this->mediaHelper->getSwatchAttributeImage($type, $filename);
}
}
diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml
index 97a391137d8e3..5f3ec07bd4983 100644
--- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml
+++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml
@@ -19,6 +19,7 @@
+
@@ -41,6 +42,7 @@
+
diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml
new file mode 100644
index 0000000000000..c6266e034bffc
--- /dev/null
+++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php
index 4056bf27f571e..06960c409b476 100644
--- a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php
+++ b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php
@@ -18,6 +18,7 @@
use Magento\Swatches\Block\LayeredNavigation\RenderLayered;
use Magento\Swatches\Helper\Data;
use Magento\Swatches\Helper\Media;
+use Magento\Theme\Block\Html\Pager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -28,35 +29,60 @@
*/
class RenderLayeredTest extends TestCase
{
- /** @var MockObject */
- protected $contextMock;
-
- /** @var MockObject */
- protected $requestMock;
-
- /** @var MockObject */
- protected $urlBuilder;
-
- /** @var MockObject */
- protected $eavAttributeMock;
-
- /** @var MockObject */
- protected $layerAttributeFactoryMock;
-
- /** @var MockObject */
- protected $layerAttributeMock;
-
- /** @var MockObject */
- protected $swatchHelperMock;
-
- /** @var MockObject */
- protected $mediaHelperMock;
-
- /** @var MockObject */
- protected $filterMock;
-
- /** @var MockObject */
- protected $block;
+ /**
+ * @var RenderLayered|MockObject
+ */
+ private $block;
+
+ /**
+ * @var Context|MockObject
+ */
+ private $contextMock;
+
+ /**
+ * @var RequestInterface|MockObject
+ */
+ private $requestMock;
+
+ /**
+ * @var Url|MockObject
+ */
+ private $urlBuilder;
+
+ /**
+ * @var Attribute|MockObject
+ */
+ private $eavAttributeMock;
+
+ /**
+ * @var AttributeFactory|MockObject
+ */
+ private $layerAttributeFactoryMock;
+
+ /**
+ * @var \Magento\Catalog\Model\ResourceModel\Layer\Filter\Attribute|MockObject
+ */
+ private $layerAttributeMock;
+
+ /**
+ * @var Data|MockObject
+ */
+ private $swatchHelperMock;
+
+ /**
+ * @var Media|MockObject
+ */
+ private $mediaHelperMock;
+
+ /**
+ * @var AbstractFilter|MockObject
+ */
+ private $filterMock;
+
+ /**
+ * @var Pager|MockObject
+ */
+ private $htmlBlockPagerMock;
protected function setUp(): void
{
@@ -66,8 +92,8 @@ protected function setUp(): void
Url::class,
['getCurrentUrl', 'getRedirectUrl', 'getUrl']
);
- $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock);
- $this->contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilder);
+ $this->contextMock->method('getRequest')->willReturn($this->requestMock);
+ $this->contextMock->method('getUrlBuilder')->willReturn($this->urlBuilder);
$this->eavAttributeMock = $this->createMock(Attribute::class);
$this->layerAttributeFactoryMock = $this->createPartialMock(
AttributeFactory::class,
@@ -80,6 +106,7 @@ protected function setUp(): void
$this->swatchHelperMock = $this->createMock(Data::class);
$this->mediaHelperMock = $this->createMock(Media::class);
$this->filterMock = $this->createMock(AbstractFilter::class);
+ $this->htmlBlockPagerMock = $this->createMock(Pager::class);
$this->block = $this->getMockBuilder(RenderLayered::class)
->setMethods(['filter', 'eavAttribute'])
@@ -91,6 +118,7 @@ protected function setUp(): void
$this->swatchHelperMock,
$this->mediaHelperMock,
[],
+ $this->htmlBlockPagerMock
]
)
->getMock();
@@ -114,7 +142,7 @@ public function testGetSwatchData()
$item3 = $this->createMock(Item::class);
$item4 = $this->createMock(Item::class);
- $item1->expects($this->any())->method('__call')->withConsecutive(
+ $item1->method('__call')->withConsecutive(
['getValue'],
['getCount'],
['getValue'],
@@ -128,9 +156,9 @@ public function testGetSwatchData()
'Yellow'
);
- $item2->expects($this->any())->method('__call')->with('getValue')->willReturn('blue');
+ $item2->method('__call')->with('getValue')->willReturn('blue');
- $item3->expects($this->any())->method('__call')->withConsecutive(
+ $item3->method('__call')->withConsecutive(
['getValue'],
['getCount']
)->willReturnOnConsecutiveCalls(
@@ -138,7 +166,7 @@ public function testGetSwatchData()
0
);
- $item4->expects($this->any())->method('__call')->withConsecutive(
+ $item4->method('__call')->withConsecutive(
['getValue'],
['getCount'],
['getValue'],
@@ -162,22 +190,22 @@ public function testGetSwatchData()
$this->block->method('filter')->willReturn($this->filterMock);
$option1 = $this->createMock(Option::class);
- $option1->expects($this->any())->method('getValue')->willReturn('yellow');
+ $option1->method('getValue')->willReturn('yellow');
$option2 = $this->createMock(Option::class);
- $option2->expects($this->any())->method('getValue')->willReturn(null);
+ $option2->method('getValue')->willReturn(null);
$option3 = $this->createMock(Option::class);
- $option3->expects($this->any())->method('getValue')->willReturn('red');
+ $option3->method('getValue')->willReturn('red');
$option4 = $this->createMock(Option::class);
- $option4->expects($this->any())->method('getValue')->willReturn('green');
+ $option4->method('getValue')->willReturn('green');
$eavAttribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class);
$eavAttribute->expects($this->once())
->method('getOptions')
->willReturn([$option1, $option2, $option3, $option4]);
- $eavAttribute->expects($this->any())->method('getIsFilterable')->willReturn(0);
+ $eavAttribute->method('getIsFilterable')->willReturn(0);
$this->filterMock->expects($this->once())->method('getAttributeModel')->willReturn($eavAttribute);
$this->block->method('eavAttribute')->willReturn($eavAttribute);
@@ -200,7 +228,7 @@ public function testGetSwatchDataException()
{
$this->block->method('filter')->willReturn($this->filterMock);
$this->block->setSwatchFilter($this->filterMock);
- $this->expectException('\RuntimeException');
+ $this->expectException(\RuntimeException::class);
$this->block->getSwatchData();
}
diff --git a/app/code/Magento/Theme/Block/Html/Pager.php b/app/code/Magento/Theme/Block/Html/Pager.php
index 5798b94e31a70..764b2e9ca42f0 100644
--- a/app/code/Magento/Theme/Block/Html/Pager.php
+++ b/app/code/Magento/Theme/Block/Html/Pager.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\Theme\Block\Html;
/**
@@ -466,7 +467,26 @@ public function getPageUrl($page)
*/
public function getLimitUrl($limit)
{
- return $this->getPagerUrl([$this->getLimitVarName() => $limit]);
+ return $this->getPagerUrl($this->getPageLimitParams($limit));
+ }
+
+ /**
+ * Return page limit params
+ *
+ * @param int $limit
+ * @return array
+ */
+ private function getPageLimitParams(int $limit): array
+ {
+ $data = [$this->getLimitVarName() => $limit];
+
+ $currentPage = $this->getCurrentPage();
+ $availableCount = (int) ceil($this->getTotalNum() / $limit);
+ if ($currentPage !== 1 && $availableCount < $currentPage) {
+ $data = array_merge($data, [$this->getPageVarName() => $availableCount === 1 ? null : $availableCount]);
+ }
+
+ return $data;
}
/**
diff --git a/app/code/Magento/Theme/Model/Config/Customization.php b/app/code/Magento/Theme/Model/Config/Customization.php
index 6a6872d794b1b..7430730451110 100644
--- a/app/code/Magento/Theme/Model/Config/Customization.php
+++ b/app/code/Magento/Theme/Model/Config/Customization.php
@@ -5,23 +5,34 @@
*/
namespace Magento\Theme\Model\Config;
+use Magento\Framework\App\Area;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\View\Design\Theme\ThemeProviderInterface;
+use Magento\Framework\View\Design\ThemeInterface;
+use Magento\Framework\View\DesignInterface;
+use Magento\Store\Model\Store;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Theme\Model\ResourceModel\Theme\Collection;
+use Magento\Theme\Model\Theme\StoreThemesResolverInterface;
+use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver;
+
/**
* Theme customization config model
*/
class Customization
{
/**
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $_storeManager;
/**
- * @var \Magento\Framework\View\DesignInterface
+ * @var DesignInterface
*/
protected $_design;
/**
- * @var \Magento\Framework\View\Design\Theme\ThemeProviderInterface
+ * @var ThemeProviderInterface
*/
protected $themeProvider;
@@ -40,20 +51,28 @@ class Customization
* @see self::_prepareThemeCustomizations()
*/
protected $_unassignedTheme;
+ /**
+ * @var StoreUserAgentThemeResolver|mixed|null
+ */
+ private $storeThemesResolver;
/**
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\View\DesignInterface $design
- * @param \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider
+ * @param StoreManagerInterface $storeManager
+ * @param DesignInterface $design
+ * @param ThemeProviderInterface $themeProvider
+ * @param StoreThemesResolverInterface|null $storeThemesResolver
*/
public function __construct(
- \Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\View\DesignInterface $design,
- \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider
+ StoreManagerInterface $storeManager,
+ DesignInterface $design,
+ ThemeProviderInterface $themeProvider,
+ ?StoreThemesResolverInterface $storeThemesResolver = null
) {
$this->_storeManager = $storeManager;
$this->_design = $design;
$this->themeProvider = $themeProvider;
+ $this->storeThemesResolver = $storeThemesResolver
+ ?? ObjectManager::getInstance()->get(StoreThemesResolverInterface::class);
}
/**
@@ -93,13 +112,14 @@ public function getStoresByThemes()
{
$storesByThemes = [];
$stores = $this->_storeManager->getStores();
- /** @var $store \Magento\Store\Model\Store */
+ /** @var $store Store */
foreach ($stores as $store) {
- $themeId = $this->_getConfigurationThemeId($store);
- if (!isset($storesByThemes[$themeId])) {
- $storesByThemes[$themeId] = [];
+ foreach ($this->storeThemesResolver->getThemes($store) as $themeId) {
+ if (!isset($storesByThemes[$themeId])) {
+ $storesByThemes[$themeId] = [];
+ }
+ $storesByThemes[$themeId][] = $store;
}
- $storesByThemes[$themeId][] = $store;
}
return $storesByThemes;
}
@@ -107,8 +127,8 @@ public function getStoresByThemes()
/**
* Check if current theme has assigned to any store
*
- * @param \Magento\Framework\View\Design\ThemeInterface $theme
- * @param null|\Magento\Store\Model\Store $store
+ * @param ThemeInterface $theme
+ * @param null|Store $store
* @return bool
*/
public function isThemeAssignedToStore($theme, $store = null)
@@ -133,8 +153,8 @@ public function hasThemeAssigned()
/**
* Is theme assigned to specific store
*
- * @param \Magento\Framework\View\Design\ThemeInterface $theme
- * @param \Magento\Store\Model\Store $store
+ * @param ThemeInterface $theme
+ * @param Store $store
* @return bool
*/
protected function _isThemeAssignedToSpecificStore($theme, $store)
@@ -145,21 +165,21 @@ protected function _isThemeAssignedToSpecificStore($theme, $store)
/**
* Get configuration theme id
*
- * @param \Magento\Store\Model\Store $store
+ * @param Store $store
* @return int
*/
protected function _getConfigurationThemeId($store)
{
return $this->_design->getConfigurationDesignTheme(
- \Magento\Framework\App\Area::AREA_FRONTEND,
+ Area::AREA_FRONTEND,
['store' => $store]
);
}
/**
* Fetch theme customization and sort them out to arrays:
- * self::_assignedTheme and self::_unassignedTheme.
*
+ * Set self::_assignedTheme and self::_unassignedTheme.
* NOTE: To get into "assigned" list theme customization not necessary should be assigned to store-view directly.
* It can be set to website or as default theme and be used by store-view via config fallback mechanism.
*
@@ -167,15 +187,15 @@ protected function _getConfigurationThemeId($store)
*/
protected function _prepareThemeCustomizations()
{
- /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection */
- $themeCollection = $this->themeProvider->getThemeCustomizations(\Magento\Framework\App\Area::AREA_FRONTEND);
+ /** @var Collection $themeCollection */
+ $themeCollection = $this->themeProvider->getThemeCustomizations(Area::AREA_FRONTEND);
$assignedThemes = $this->getStoresByThemes();
$this->_assignedTheme = [];
$this->_unassignedTheme = [];
- /** @var $theme \Magento\Framework\View\Design\ThemeInterface */
+ /** @var $theme ThemeInterface */
foreach ($themeCollection as $theme) {
if (isset($assignedThemes[$theme->getId()])) {
$theme->setAssignedStores($assignedThemes[$theme->getId()]);
diff --git a/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php
new file mode 100644
index 0000000000000..26bd5604294d1
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php
@@ -0,0 +1,90 @@
+design = $design;
+ $this->themeCollectionFactory = $themeCollectionFactory;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $theme = $this->design->getConfigurationDesignTheme(
+ Area::AREA_FRONTEND,
+ ['store' => $store]
+ );
+ $themes = [];
+ if ($theme) {
+ if (!is_numeric($theme)) {
+ $registeredThemes = $this->getRegisteredThemes();
+ if (isset($registeredThemes[$theme])) {
+ $themes[] = $registeredThemes[$theme]->getId();
+ }
+ } else {
+ $themes[] = $theme;
+ }
+ }
+ return $themes;
+ }
+
+ /**
+ * Get system registered themes.
+ *
+ * @return ThemeInterface[]
+ */
+ private function getRegisteredThemes(): array
+ {
+ if ($this->registeredThemes === null) {
+ $this->registeredThemes = [];
+ /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $collection */
+ $collection = $this->themeCollectionFactory->create();
+ $themes = $collection->loadRegisteredThemes();
+ /** @var ThemeInterface $theme */
+ foreach ($themes as $theme) {
+ $this->registeredThemes[$theme->getCode()] = $theme;
+ }
+ }
+ return $this->registeredThemes;
+ }
+}
diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php
new file mode 100644
index 0000000000000..5be86c08f7c51
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php
@@ -0,0 +1,57 @@
+resolvers = $resolvers;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $themes = [];
+ foreach ($this->resolvers as $resolver) {
+ foreach ($resolver->getThemes($store) as $theme) {
+ $themes[] = $theme;
+ }
+ }
+ return array_values(array_unique($themes));
+ }
+}
diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php
new file mode 100644
index 0000000000000..bb2cd73300c02
--- /dev/null
+++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php
@@ -0,0 +1,24 @@
+scopeConfig = $scopeConfig;
+ $this->serializer = $serializer;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getThemes(StoreInterface $store): array
+ {
+ $config = $this->scopeConfig->getValue(
+ self::XML_PATH_THEME_USER_AGENT,
+ ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ $rules = $config ? $this->serializer->unserialize($config) : [];
+ $themes = [];
+ if ($rules) {
+ $themes = array_values(array_unique(array_column($rules, 'value')));
+ }
+ return $themes;
+ }
+}
diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
index ac16c56b17f1b..fd0ef1db0219a 100644
--- a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
+++ b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php
@@ -91,6 +91,60 @@ public function testGetPages(): void
$this->assertEquals($expectedPages, $this->pager->getPages());
}
+ /**
+ * Test get limit url.
+ *
+ * @dataProvider limitUrlDataProvider
+ *
+ * @param int $page
+ * @param int $size
+ * @param int $limit
+ * @param array $expectedParams
+ * @return void
+ */
+ public function testGetLimitUrl(int $page, int $size, int $limit, array $expectedParams): void
+ {
+ $expectedArray = [
+ '_current' => true,
+ '_escape' => true,
+ '_use_rewrite' => true,
+ '_fragment' => null,
+ '_query' => $expectedParams,
+ ];
+
+ $collectionMock = $this->createMock(Collection::class);
+ $collectionMock->expects($this->once())
+ ->method('getCurPage')
+ ->willReturn($page);
+ $collectionMock->expects($this->once())
+ ->method('getSize')
+ ->willReturn($size);
+ $this->setCollectionProperty($collectionMock);
+
+ $this->urlBuilderMock->expects($this->once())
+ ->method('getUrl')
+ ->with('*/*/*', $expectedArray);
+
+ $this->pager->getLimitUrl($limit);
+ }
+
+ /**
+ * DataProvider for testGetLimitUrl
+ *
+ * @return array
+ */
+ public function limitUrlDataProvider(): array
+ {
+ return [
+ [2, 21, 10, ['limit' => 10]],
+ [3, 21, 10, ['limit' => 10]],
+ [2, 21, 20, ['limit' => 20]],
+ [3, 21, 50, ['limit' => 50, 'p' => null]],
+ [2, 11, 20, ['limit' => 20, 'p' => null]],
+ [4, 40, 20, ['limit' => 20, 'p' => 2]],
+ ];
+ }
+
/**
* Set Collection
*
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
index 82678d4b4277d..438853b9935e6 100644
--- a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
+++ b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php
@@ -13,9 +13,10 @@
use Magento\Framework\App\Area;
use Magento\Framework\DataObject;
use Magento\Framework\View\DesignInterface;
+use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Theme\Model\Config\Customization;
-use Magento\Theme\Model\ResourceModel\Theme\Collection;
+use Magento\Theme\Model\Theme\StoreThemesResolverInterface;
use Magento\Theme\Model\Theme\ThemeProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -32,47 +33,37 @@ class CustomizationTest extends TestCase
*/
protected $designPackage;
- /**
- * @var Collection
- */
- protected $themeCollection;
-
/**
* @var Customization
*/
protected $model;
/**
- * @var ThemeProvider|\PHPUnit\Framework\MockObject_MockBuilder
+ * @var ThemeProvider|MockObject
*/
protected $themeProviderMock;
+ /**
+ * @var StoreThemesResolverInterface|MockObject
+ */
+ private $storeThemesResolver;
protected function setUp(): void
{
- $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)
- ->getMock();
- $this->designPackage = $this->getMockBuilder(DesignInterface::class)
- ->getMock();
- $this->themeCollection = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $collectionFactory = $this->getMockBuilder(\Magento\Theme\Model\ResourceModel\Theme\CollectionFactory::class)
- ->disableOriginalConstructor()
- ->setMethods(['create'])
- ->getMock();
-
- $collectionFactory->expects($this->any())->method('create')->willReturn($this->themeCollection);
+ $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock();
+ $this->designPackage = $this->getMockBuilder(DesignInterface::class)->getMock();
$this->themeProviderMock = $this->getMockBuilder(ThemeProvider::class)
->disableOriginalConstructor()
->setMethods(['getThemeCustomizations', 'getThemeByFullPath'])
->getMock();
+ $this->storeThemesResolver = $this->createMock(StoreThemesResolverInterface::class);
+
$this->model = new Customization(
$this->storeManager,
$this->designPackage,
- $this->themeProviderMock
+ $this->themeProviderMock,
+ $this->storeThemesResolver
);
}
@@ -84,13 +75,15 @@ protected function setUp(): void
*/
public function testGetAssignedThemeCustomizations()
{
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
-
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
+
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -108,13 +101,15 @@ public function testGetAssignedThemeCustomizations()
*/
public function testGetUnassignedThemeCustomizations()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -131,13 +126,15 @@ public function testGetUnassignedThemeCustomizations()
*/
public function testGetStoresByThemes()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$stores = $this->model->getStoresByThemes();
$this->assertArrayHasKey($this->getAssignedTheme()->getId(), $stores);
@@ -148,15 +145,17 @@ public function testGetStoresByThemes()
* @covers \Magento\Theme\Model\Config\Customization::_getConfigurationThemeId
* @covers \Magento\Theme\Model\Config\Customization::__construct
*/
- public function testIsThemeAssignedToDefaultStore()
+ public function testIsThemeAssignedToAnyStore()
{
+ $store = $this->getStore();
$this->storeManager->expects($this->once())
->method('getStores')
- ->willReturn([$this->getStore()]);
+ ->willReturn([$store]);
- $this->designPackage->expects($this->once())
- ->method('getConfigurationDesignTheme')
- ->willReturn($this->getAssignedTheme()->getId());
+ $this->storeThemesResolver->expects($this->once())
+ ->method('getThemes')
+ ->with($store)
+ ->willReturn([$this->getAssignedTheme()->getId()]);
$this->themeProviderMock->expects($this->once())
->method('getThemeCustomizations')
@@ -198,10 +197,10 @@ protected function getUnassignedTheme()
}
/**
- * @return DataObject
+ * @return StoreInterface|MockObject
*/
protected function getStore()
{
- return new DataObject(['id' => 55]);
+ return $this->createConfiguredMock(StoreInterface::class, ['getId' => 55]);
}
}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php
new file mode 100644
index 0000000000000..939b47a42ce85
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php
@@ -0,0 +1,115 @@
+createMock(CollectionFactory::class);
+ $this->design = $this->createMock(DesignInterface::class);
+ $this->model = new StoreDefaultThemeResolver(
+ $themeCollectionFactory,
+ $this->design
+ );
+ $registeredThemes = [];
+ $registeredThemes[] = $this->createConfiguredMock(
+ ThemeInterface::class,
+ [
+ 'getId' => 1,
+ 'getCode' => 'Magento/luma',
+ ]
+ );
+ $registeredThemes[] = $this->createConfiguredMock(
+ ThemeInterface::class,
+ [
+ 'getId' => 2,
+ 'getCode' => 'Magento/blank',
+ ]
+ );
+ $collection = $this->createMock(Collection::class);
+ $collection->method('getIterator')
+ ->willReturn(new ArrayIterator($registeredThemes));
+ $collection->method('loadRegisteredThemes')
+ ->willReturnSelf();
+ $themeCollectionFactory->method('create')
+ ->willReturn($collection);
+ }
+
+ /**
+ * Test that method returns default theme associated to given store.
+ *
+ * @param string|null $defaultTheme
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(?string $defaultTheme, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ $this->design->expects($this->once())
+ ->method('getConfigurationDesignTheme')
+ ->with(
+ Area::AREA_FRONTEND,
+ ['store' => $store]
+ )
+ ->willReturn($defaultTheme);
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ null,
+ []
+ ],
+ [
+ '1',
+ [1]
+ ],
+ [
+ 'Magento/blank',
+ [2]
+ ],
+ [
+ 'Magento/theme',
+ []
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php
new file mode 100644
index 0000000000000..b80ec4ae83887
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php
@@ -0,0 +1,115 @@
+resolvers = [];
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class);
+ $this->model = new StoreThemesResolver($this->resolvers);
+ }
+
+ /**
+ * Test that constructor SHOULD throw an exception when resolver is not instance of StoreThemesResolverInterface.
+ */
+ public function testInvalidConstructorArguments(): void
+ {
+ $resolver = $this->createMock(StoreInterface::class);
+ $this->expectExceptionObject(
+ new \InvalidArgumentException(
+ sprintf(
+ 'Instance of %s is expected, got %s instead.',
+ StoreThemesResolverInterface::class,
+ get_class($resolver)
+ )
+ )
+ );
+ $this->model = new StoreThemesResolver(
+ [
+ $resolver
+ ]
+ );
+ }
+
+ /**
+ * Test that method returns aggregated themes from resolvers
+ *
+ * @param array $themes
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(array $themes, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ foreach ($this->resolvers as $key => $resolver) {
+ $resolver->expects($this->once())
+ ->method('getThemes')
+ ->willReturn($themes[$key]);
+ }
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ [
+ [],
+ [],
+ []
+ ],
+ []
+ ],
+ [
+ [
+ ['1'],
+ [],
+ ['1']
+ ],
+ ['1']
+ ],
+ [
+ [
+ ['1'],
+ ['2'],
+ ['1']
+ ],
+ ['1', '2']
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php
new file mode 100644
index 0000000000000..1ef4b17ca6562
--- /dev/null
+++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php
@@ -0,0 +1,105 @@
+scopeConfig = $this->createMock(ScopeConfigInterface::class);
+ $this->serializer = new Json();
+ $this->model = new StoreUserAgentThemeResolver(
+ $this->scopeConfig,
+ $this->serializer
+ );
+ }
+
+ /**
+ * Test that method returns user-agent rules associated themes.
+ *
+ * @param array|null $config
+ * @param array $expected
+ * @dataProvider getThemesDataProvider
+ */
+ public function testGetThemes(?array $config, array $expected): void
+ {
+ $store = $this->createMock(StoreInterface::class);
+ $this->scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->with('design/theme/ua_regexp', ScopeInterface::SCOPE_STORE, $store)
+ ->willReturn($config !== null ? $this->serializer->serialize($config) : $config);
+ $this->assertEquals($expected, $this->model->getThemes($store));
+ }
+
+ /**
+ * @return array
+ */
+ public function getThemesDataProvider(): array
+ {
+ return [
+ [
+ null,
+ []
+ ],
+ [
+ [],
+ []
+ ],
+ [
+ [
+ [
+ 'search' => '\/Chrome\/i',
+ 'regexp' => '\/Chrome\/i',
+ 'value' => '1',
+ ],
+ ],
+ ['1']
+ ],
+ [
+ [
+ [
+ 'search' => '\/Chrome\/i',
+ 'regexp' => '\/Chrome\/i',
+ 'value' => '1',
+ ],
+ [
+ 'search' => '\/mozila\/i',
+ 'regexp' => '\/mozila\/i',
+ 'value' => '2',
+ ],
+ ],
+ ['1', '2']
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml
index 921e6bfc6ecf1..c4da1f860870e 100644
--- a/app/code/Magento/Theme/etc/di.xml
+++ b/app/code/Magento/Theme/etc/di.xml
@@ -18,6 +18,7 @@
+
Magento\Framework\App\Cache\Type\Config
@@ -309,4 +310,12 @@
configured_design_cache
+
+
+
+ - Magento\Theme\Model\Theme\StoreDefaultThemeResolver
+ - Magento\Theme\Model\Theme\StoreUserAgentThemeResolver
+
+
+
diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml
new file mode 100644
index 0000000000000..a409860811837
--- /dev/null
+++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Assert the target path is shown in the URL Rewrite grid.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml
index 4e46ed8e4fc79..3b140aed5f572 100644
--- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml
+++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml
@@ -47,84 +47,103 @@
+
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/User/Model/Notificator.php b/app/code/Magento/User/Model/Notificator.php
index 3a5522db4c533..3e36cd1387e39 100644
--- a/app/code/Magento/User/Model/Notificator.php
+++ b/app/code/Magento/User/Model/Notificator.php
@@ -107,6 +107,7 @@ public function sendForgotPassword(UserInterface $user): void
$this->sendNotification(
'admin/emails/forgot_email_template',
[
+ 'username' => $user->getFirstName().' '.$user->getLastName(),
'user' => $user,
'store' => $this->storeManager->getStore(
Store::DEFAULT_STORE_ID
diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml
similarity index 83%
rename from app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml
rename to app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml
index 4049e60e83455..d41ed63678783 100644
--- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml
+++ b/app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml
@@ -5,10 +5,8 @@
* See COPYING.txt for license details.
*/
-->
-
-
-
+
+
Goes to the Backend Admin Login page. Fill Username and Password. Click on Sign In.
diff --git a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html
index dacfa640464a3..42240bff3b8db 100644
--- a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html
+++ b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html
@@ -4,16 +4,17 @@
* See COPYING.txt for license details.
*/
-->
-
+
-{{trans "%name," name=$user.name}}
+{{trans "%name," name=$username}}
{{trans "There was recently a request to change the password for your account."}}
diff --git a/app/code/Magento/Wishlist/Model/Wishlist.php b/app/code/Magento/Wishlist/Model/Wishlist.php
index 9b7ff5177afae..cb1a7d956570b 100644
--- a/app/code/Magento/Wishlist/Model/Wishlist.php
+++ b/app/code/Magento/Wishlist/Model/Wishlist.php
@@ -181,6 +181,7 @@ class Wishlist extends AbstractModel implements IdentityInterface
* @param Json|null $serializer
* @param StockRegistryInterface|null $stockRegistry
* @param ScopeConfigInterface|null $scopeConfig
+ *
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -226,6 +227,7 @@ public function __construct(
*
* @param int $customerId
* @param bool $create Create wishlist if don't exists
+ *
* @return $this
*/
public function loadByCustomerId($customerId, $create = false)
@@ -274,6 +276,7 @@ public function generateSharingCode()
* Load by sharing code
*
* @param string $code
+ *
* @return $this
*/
public function loadByCode($code)
@@ -370,6 +373,7 @@ protected function _addCatalogProduct(Product $product, $qty = 1, $forciblySetQt
* Retrieve wishlist item collection
*
* @return \Magento\Wishlist\Model\ResourceModel\Item\Collection
+ *
* @throws NoSuchEntityException
*/
public function getItemCollection()
@@ -389,6 +393,7 @@ public function getItemCollection()
* Retrieve wishlist item collection
*
* @param int $itemId
+ *
* @return false|Item
*/
public function getItem($itemId)
@@ -403,7 +408,9 @@ public function getItem($itemId)
* Adding item to wishlist
*
* @param Item $item
+ *
* @return $this
+ *
* @throws Exception
*/
public function addItem(Item $item)
@@ -424,9 +431,12 @@ public function addItem(Item $item)
* @param int|Product $product
* @param DataObject|array|string|null $buyRequest
* @param bool $forciblySetQty
+ *
* @return Item|string
+ *
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
+ *
* @throws LocalizedException
* @throws InvalidArgumentException
*/
@@ -529,7 +539,9 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false
* Set customer id
*
* @param int $customerId
+ *
* @return $this
+ *
* @throws LocalizedException
*/
public function setCustomerId($customerId)
@@ -541,6 +553,7 @@ public function setCustomerId($customerId)
* Retrieve customer id
*
* @return int
+ *
* @throws LocalizedException
*/
public function getCustomerId()
@@ -552,6 +565,7 @@ public function getCustomerId()
* Retrieve data for save
*
* @return array
+ *
* @throws LocalizedException
*/
public function getDataForSave()
@@ -567,6 +581,7 @@ public function getDataForSave()
* Retrieve shared store ids for current website or all stores if $current is false
*
* @return array
+ *
* @throws NoSuchEntityException
*/
public function getSharedStoreIds()
@@ -590,6 +605,7 @@ public function getSharedStoreIds()
* Set shared store ids
*
* @param array $storeIds
+ *
* @return $this
*/
public function setSharedStoreIds($storeIds)
@@ -602,6 +618,7 @@ public function setSharedStoreIds($storeIds)
* Retrieve wishlist store object
*
* @return \Magento\Store\Model\Store
+ *
* @throws NoSuchEntityException
*/
public function getStore()
@@ -616,6 +633,7 @@ public function getStore()
* Set wishlist store
*
* @param Store $store
+ *
* @return $this
*/
public function setStore($store)
@@ -653,6 +671,7 @@ public function isSalable()
* Retrieve if product has stock or config is set for showing out of stock products
*
* @param int $productId
+ *
* @return bool
*/
private function isInStock($productId)
@@ -671,7 +690,9 @@ private function isInStock($productId)
* Check customer is owner this wishlist
*
* @param int $customerId
+ *
* @return bool
+ *
* @throws LocalizedException
*/
public function isOwner($customerId)
@@ -696,10 +717,13 @@ public function isOwner($customerId)
* @param int|Item $itemId
* @param DataObject $buyRequest
* @param null|array|DataObject $params
+ *
* @return $this
+ *
* @throws LocalizedException
*
* @see \Magento\Catalog\Helper\Product::addParamsToBuyRequest()
+ *
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
@@ -748,10 +772,11 @@ public function updateItem($itemId, $buyRequest, $params = null)
throw new LocalizedException(__($resultItem));
}
+ if ($resultItem->getDescription() != $item->getDescription()) {
+ $resultItem->setDescription($item->getDescription())->save();
+ }
+
if ($resultItem->getId() != $itemId) {
- if ($resultItem->getDescription() != $item->getDescription()) {
- $resultItem->setDescription($item->getDescription())->save();
- }
$item->isDeleted(true);
$this->setDataChanges(true);
} else {
diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less
index f57420deb621d..4b48bbe99ced2 100644
--- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less
+++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less
@@ -457,11 +457,26 @@
.action {
&.delete {
&:extend(.abs-remove-button-for-blocks all);
- line-height: unset;
position: absolute;
right: 0;
top: -1px;
- width: auto;
+ }
+ }
+
+ .block-wishlist {
+ .action {
+ &.delete {
+ line-height: unset;
+ width: auto;
+ }
+ }
+ }
+
+ .block-compare {
+ .action {
+ &.delete {
+ right: initial;
+ }
}
}
@@ -814,6 +829,7 @@
&:extend(.abs-remove-button-for-blocks all);
left: -6px;
position: absolute;
+ right: 0;
top: 0;
}
diff --git a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less
index 09759d95c4b10..8434812f20719 100644
--- a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less
+++ b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less
@@ -82,6 +82,10 @@
.field {
margin-right: 5px;
+ &.newsletter {
+ max-width: 220px;
+ }
+
.control {
width: 100%;
}
diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less
index d0b7aa1523ad6..e205b20efd17c 100644
--- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less
+++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less
@@ -998,6 +998,15 @@
}
}
}
+
+ .block-compare {
+ .action {
+ &.delete {
+ left: 0;
+ right: initial;
+ }
+ }
+ }
}
}
@@ -1005,6 +1014,7 @@
.compare.wrapper {
display: none;
}
+
.catalog-product_compare-index {
.columns {
.column {
diff --git a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less
index a72f31d72ce48..21ed451a69d10 100644
--- a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less
+++ b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less
@@ -81,6 +81,10 @@
.block.newsletter {
max-width: 44%;
width: max-content;
+
+ .field.newsletter {
+ max-width: 220px;
+ }
.form.subscribe {
> .field,
diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html
index 024f6daf76ace..e51b952281ed5 100644
--- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html
+++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html
@@ -8,7 +8,7 @@