= $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/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/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 @@