diff --git a/app/code/Magento/Bundle/Model/Product/LinksList.php b/app/code/Magento/Bundle/Model/Product/LinksList.php
index aeb71d0e434ab..c35d475e04d84 100644
--- a/app/code/Magento/Bundle/Model/Product/LinksList.php
+++ b/app/code/Magento/Bundle/Model/Product/LinksList.php
@@ -39,6 +39,8 @@ public function __construct(
}
/**
+ * Bundle Product Items Data
+ *
* @param \Magento\Catalog\Api\Data\ProductInterface $product
* @param int $optionId
* @return \Magento\Bundle\Api\Data\LinkInterface[]
@@ -50,8 +52,12 @@ public function getItems(\Magento\Catalog\Api\Data\ProductInterface $product, $o
$productLinks = [];
/** @var \Magento\Catalog\Model\Product $selection */
foreach ($selectionCollection as $selection) {
+ $bundledProductPrice = $selection->getSelectionPriceValue();
+ if ($bundledProductPrice <= 0) {
+ $bundledProductPrice = $selection->getPrice();
+ }
$selectionPriceType = $product->getPriceType() ? $selection->getSelectionPriceType() : null;
- $selectionPrice = $product->getPriceType() ? $selection->getSelectionPriceValue() : null;
+ $selectionPrice = $bundledProductPrice ? $bundledProductPrice : null;
/** @var \Magento\Bundle\Api\Data\LinkInterface $productLink */
$productLink = $this->linkFactory->create();
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml
new file mode 100644
index 0000000000000..0e2ae9bf5cc5f
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php
index fbc3b5e87ac97..27531682b1de2 100644
--- a/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php
@@ -91,7 +91,7 @@ public function testLinksList()
->method('getSelectionsCollection')
->with([$optionId], $this->productMock)
->willReturn([$this->selectionMock]);
- $this->productMock->expects($this->exactly(2))->method('getPriceType')->willReturn('price_type');
+ $this->productMock->expects($this->once())->method('getPriceType')->willReturn('price_type');
$this->selectionMock->expects($this->once())
->method('getSelectionPriceType')
->willReturn('selection_price_type');
diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml
index 23e7ef27fa78d..15ef3c311e396 100644
--- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml
+++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml
@@ -15,6 +15,7 @@
getChildren($_item); ?>
+
getData('catalogHelper');
@@ -37,7 +38,7 @@ $catalogHelper = $block->getData('catalogHelper');
getOrderItem()->getParentItem()): ?>
getSelectionAttributes($_item) ?>
@@ -130,7 +131,7 @@ $catalogHelper = $block->getData('catalogHelper');
canShowPriceInfo($_item) || $shipTogether): ?>
- canEditQty()): ?>
+ canEditQty() && $canEditItemQty): ?>
(int)$product->getId(),
'priceFormat' => $this->_localeFormat->getPriceFormat(),
'prices' => [
+ 'baseOldPrice' => [
+ 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() * 1,
+ 'adjustments' => []
+ ],
'oldPrice' => [
'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getValue() * 1,
'adjustments' => []
diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php
index a06266037d05c..ab74b5694ce9f 100644
--- a/app/code/Magento/Catalog/Helper/Image.php
+++ b/app/code/Magento/Catalog/Helper/Image.php
@@ -384,7 +384,9 @@ public function backgroundColor($colorRGB)
{
// assume that 3 params were given instead of array
if (!is_array($colorRGB)) {
+ //phpcs:disable
$colorRGB = func_get_args();
+ //phpcs:enabled
}
$this->_getModel()->setBackgroundColor($colorRGB);
return $this;
@@ -498,7 +500,11 @@ protected function initBaseFile()
if ($this->getImageFile()) {
$model->setBaseFile($this->getImageFile());
} else {
- $model->setBaseFile($this->getProduct()->getData($model->getDestinationSubdir()));
+ $model->setBaseFile(
+ $this->getProduct()
+ ? $this->getProduct()->getData($model->getDestinationSubdir())
+ : ''
+ );
}
}
return $this;
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml
index 2eda7b8d02481..bfa80c2e24b48 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml
@@ -20,8 +20,14 @@
-
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php
index aa29972c91a62..c606b7537cc44 100644
--- a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php
@@ -396,6 +396,14 @@ public function testGetWidth()
$this->assertEquals($data['width'], $this->helper->getWidth());
}
+ /**
+ * Check initBaseFile without properties - product
+ */
+ public function testGetUrlWithOutProduct()
+ {
+ $this->assertNull($this->helper->getUrl());
+ }
+
/**
* @param array $data
* @dataProvider getHeightDataProvider
diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php
index d3c8c406ee34d..2aa30fb18fdf4 100644
--- a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php
+++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php
@@ -71,7 +71,7 @@ public function __construct(
public function getCategoryUrlSuffix()
{
return $this->scopeConfig->getValue(
- static::XML_PATH_CATEGORY_URL_SUFFIX,
+ self::XML_PATH_CATEGORY_URL_SUFFIX,
ScopeInterface::SCOPE_STORE
);
}
@@ -84,7 +84,7 @@ public function getCategoryUrlSuffix()
public function isCategoryUsedInProductUrl(): bool
{
return $this->scopeConfig->isSetFlag(
- static::XML_PATH_PRODUCT_USE_CATEGORIES,
+ self::XML_PATH_PRODUCT_USE_CATEGORIES,
ScopeInterface::SCOPE_STORE
);
}
diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml
new file mode 100644
index 0000000000000..e146506d51a24
--- /dev/null
+++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Fill catalog products list title field.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml
new file mode 100644
index 0000000000000..4505680424471
--- /dev/null
+++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Assert widget title on storefront.
+
+
+
+
+
+
+
+ $grabWidgetTitle
+ {{title}}
+
+
+
diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml
index 9b40971611d6f..3d8d5ecc1cda9 100644
--- a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml
+++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml
@@ -19,5 +19,6 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml
new file mode 100644
index 0000000000000..28e779f802cde
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php
index 29f84e0b2e534..1f991bb47c6fd 100644
--- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php
+++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php
@@ -10,7 +10,6 @@
namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images;
use Magento\Framework\App\Action\HttpPostActionInterface;
-use Magento\Framework\App\Filesystem\DirectoryList;
/**
* Delete image folder.
diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php
index 82d200beb6dc9..706718455a523 100644
--- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php
+++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php
@@ -65,7 +65,7 @@ public function execute()
}
/** @var \Magento\Framework\Controller\Result\Json $resultJson */
$resultJson = $this->resultJsonFactory->create();
-
+
return $resultJson->setData($result);
}
}
diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php
index 9bad371aa84d7..260755ea7d562 100644
--- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php
+++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php
@@ -74,7 +74,7 @@ public function execute()
}
/** @var \Magento\Framework\Controller\Result\Json $resultJson */
$resultJson = $this->resultJsonFactory->create();
-
+
return $resultJson->setData($response);
}
}
diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml
index 7e907b5b395a4..68eca3b429e2b 100644
--- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml
+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml
@@ -8,6 +8,9 @@
+
+ Open CMS edit page.
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml
new file mode 100644
index 0000000000000..bc379ec424fce
--- /dev/null
+++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php
index 636ff85d12e24..efd0edd23ad11 100644
--- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php
@@ -303,6 +303,11 @@ protected function getOptionPrices()
$prices[$product->getId()] =
[
+ 'baseOldPrice' => [
+ 'amount' => $this->localeFormat->getNumber(
+ $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount()
+ ),
+ ],
'oldPrice' => [
'amount' => $this->localeFormat->getNumber(
$priceInfo->getPrice('regular_price')->getAmount()->getValue()
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php
index b8a948d55f11a..492c5de55ad7f 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php
@@ -39,6 +39,9 @@ public function getFormattedPrices(\Magento\Framework\Pricing\PriceInfo\Base $pr
$finalPrice = $priceInfo->getPrice('final_price');
return [
+ 'baseOldPrice' => [
+ 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getBaseAmount()),
+ ],
'oldPrice' => [
'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getValue()),
],
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php
index 33b7cbe35b391..08279c55c5b30 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Block/Product/View/Type/ConfigurableTest.php
@@ -254,8 +254,11 @@ public function cacheKeyProvider(): array
* @param string|null $priceCurrency
* @param int|null $customerGroupId
*/
- public function testGetCacheKeyInfo(array $expected, ?string $priceCurrency = null, ?int $customerGroupId = null)
- {
+ public function testGetCacheKeyInfo(
+ array $expected,
+ ?string $priceCurrency = null,
+ ?int $customerGroupId = null
+ ): void {
$storeMock = $this->getMockBuilder(StoreInterface::class)
->setMethods(['getCurrentCurrency'])
->getMockForAbstractClass();
@@ -282,7 +285,7 @@ public function testGetCacheKeyInfo(array $expected, ?string $priceCurrency = nu
/**
* Check that getJsonConfig() method returns expected value
*/
- public function testGetJsonConfig()
+ public function testGetJsonConfig(): void
{
$productId = 1;
$amount = 10.50;
@@ -347,6 +350,9 @@ public function testGetJsonConfig()
->with($priceInfoMock)
->willReturn(
[
+ 'baseOldPrice' => [
+ 'amount' => $amount,
+ ],
'oldPrice' => [
'amount' => $amount,
],
@@ -386,6 +392,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage):
'currencyFormat' => '%s',
'optionPrices' => [
$productId => [
+ 'baseOldPrice' => [
+ 'amount' => $amount,
+ ],
'oldPrice' => [
'amount' => $amount,
],
@@ -403,12 +412,15 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage):
],
],
'msrpPrice' => [
- 'amount' => null ,
+ 'amount' => null,
]
],
],
'priceFormat' => [],
'prices' => [
+ 'baseOldPrice' => [
+ 'amount' => $amount,
+ ],
'oldPrice' => [
'amount' => $amount,
],
@@ -434,7 +446,7 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage):
* @param MockObject $productMock
* @return MockObject
*/
- private function getProductTypeMock(MockObject $productMock)
+ private function getProductTypeMock(MockObject $productMock): MockObject
{
$currencyMock = $this->getMockBuilder(Currency::class)
->disableOriginalConstructor()
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php
index aa546ae7ad728..c6aa9dc8e20c0 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php
@@ -36,9 +36,12 @@ protected function setUp(): void
);
}
- public function testGetFormattedPrices()
+ public function testGetFormattedPrices(): void
{
$expected = [
+ 'baseOldPrice' => [
+ 'amount' => 1000
+ ],
'oldPrice' => [
'amount' => 500
],
@@ -60,8 +63,8 @@ public function testGetFormattedPrices()
$this->localeFormatMock->expects($this->atLeastOnce())
->method('getNumber')
- ->withConsecutive([500], [1000], [500])
- ->will($this->onConsecutiveCalls(500, 1000, 500));
+ ->withConsecutive([1000], [500], [1000], [500])
+ ->will($this->onConsecutiveCalls(1000, 500, 1000, 500));
$this->assertEquals($expected, $this->model->getFormattedPrices($priceInfoMock));
}
diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml
index eee9f742a59a4..e9d0c065fd8bf 100644
--- a/app/code/Magento/Contact/view/frontend/templates/form.phtml
+++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml
@@ -69,8 +69,8 @@ $viewModel = $block->getViewModel();
class="input-text"
cols="5"
rows="3"
- data-validate="{required:true}">= $block->escapeHtml($viewModel->getUserComment()) ?>
-
+ data-validate="{required:true}"
+ >= $block->escapeHtml($viewModel->getUserComment()) ?>
= $block->getChildHtml('form.additional.info') ?>
diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml
index f26b6feea3b3b..609b435f8b39c 100644
--- a/app/code/Magento/Cron/etc/db_schema.xml
+++ b/app/code/Magento/Cron/etc/db_schema.xml
@@ -28,5 +28,9 @@
+
+
+
+
diff --git a/app/code/Magento/Cron/etc/db_schema_whitelist.json b/app/code/Magento/Cron/etc/db_schema_whitelist.json
index c8666896627e2..f0d6ebed8290f 100644
--- a/app/code/Magento/Cron/etc/db_schema_whitelist.json
+++ b/app/code/Magento/Cron/etc/db_schema_whitelist.json
@@ -12,10 +12,11 @@
},
"index": {
"CRON_SCHEDULE_JOB_CODE": true,
- "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true
+ "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true,
+ "CRON_SCHEDULE_SCHEDULE_ID_STATUS": true
},
"constraint": {
"PRIMARY": true
}
}
-}
\ No newline at end of file
+}
diff --git a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php
index ec73ac0cf7aa5..9e7a2b69f20a5 100644
--- a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php
+++ b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php
@@ -41,7 +41,14 @@ protected function _prepareLayout()
]
);
- $onClick = "setLocation('" . $this->getUrl('adminhtml/system_config/edit/section/currency') . "')";
+ $currencyOptionPath = $this->getUrl(
+ 'adminhtml/system_config/edit',
+ [
+ 'section' => 'currency',
+ '_fragment' => 'currency_options-link'
+ ]
+ );
+ $onClick = "setLocation('$currencyOptionPath')";
$this->getToolbar()->addChild(
'options_button',
diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.xml
similarity index 59%
rename from app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml
rename to app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.xml
index 0f87ee90b7ce0..39f37c745998e 100644
--- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml
+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.xml
@@ -5,12 +5,11 @@
* See COPYING.txt for license details.
*/
-->
+
-
-
-
-
-
+
+
+
diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml
index bc80a51c41c47..10f345ec69369 100644
--- a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml
+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml
new file mode 100644
index 0000000000000..4e0eb72df3aa5
--- /dev/null
+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {$grabClass}
+ open
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php
index aa7cd06666121..4b86df94b4556 100644
--- a/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php
+++ b/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php
@@ -7,15 +7,22 @@
namespace Magento\CurrencySymbol\Test\Unit\Block\Adminhtml\System;
+use Magento\Backend\Block\Template\Context;
use Magento\Backend\Block\Widget\Button;
use Magento\CurrencySymbol\Block\Adminhtml\System\Currency;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Framework\View\Element\BlockInterface;
use Magento\Framework\View\LayoutInterface;
use PHPUnit\Framework\TestCase;
+use Magento\Framework\UrlInterface;
class CurrencyTest extends TestCase
{
+ /**
+ * Stub currency option link url
+ */
+ const STUB_OPTION_LINK_URL = 'https://localhost/admin/system_config/edit/section/currency#currency_options-link';
+
/**
* Object manager helper
*
@@ -70,12 +77,25 @@ public function testPrepareLayout()
]
);
+ $contextMock = $this->createMock(Context::class);
+ $urlBuilderMock = $this->createMock(UrlInterface::class);
+
+ $contextMock->expects($this->once())->method('getUrlBuilder')->willReturn($urlBuilderMock);
+
+ $urlBuilderMock->expects($this->once())->method('getUrl')->with(
+ 'adminhtml/system_config/edit',
+ [
+ 'section' => 'currency',
+ '_fragment' => 'currency_options-link'
+ ]
+ )->willReturn(self::STUB_OPTION_LINK_URL);
+
$childBlockMock->expects($this->at(1))
->method('addChild')
->with(
'options_button',
Button::class,
- ['label' => __('Options'), 'onclick' => 'setLocation(\'\')']
+ ['label' => __('Options'), 'onclick' => 'setLocation(\''.self::STUB_OPTION_LINK_URL.'\')']
);
$childBlockMock->expects($this->at(2))
@@ -90,7 +110,8 @@ public function testPrepareLayout()
$block = $this->objectManagerHelper->getObject(
Currency::class,
[
- 'layout' => $layoutMock
+ 'layout' => $layoutMock,
+ 'context' => $contextMock
]
);
$block->setLayout($layoutMock);
diff --git a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml
index a16a3aae14b49..a377cd8ae6722 100644
--- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml
+++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml
@@ -135,7 +135,7 @@ require([
content: "{$block->escapeJs(__('Are you sure you want to strip tags?'))}",
actions: {
confirm: function () {
- this.unconvertedText = $('template_text').value;
+ self.unconvertedText = $('template_text').value;
$('convert_button').hide();
$('template_text').value = $('template_text').value.stripScripts().replace(
new RegExp('', 'img'), ''
diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php
index bcce3514e4ad9..b01c02cae4234 100644
--- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php
+++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php
@@ -7,6 +7,11 @@
namespace Magento\MediaContentSynchronization\Model;
+use Magento\AsynchronousOperations\Api\Data\OperationInterface;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Serialize\SerializerInterface;
+use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory;
+use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface;
use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface;
/**
@@ -14,24 +19,73 @@
*/
class Consume
{
+ private const ENTITY_TYPE = 'entityType';
+ private const ENTITY_ID = 'entityId';
+ private const FIELD = 'field';
+
+ /**
+ * @var SerializerInterface
+ */
+ private $serializer;
+
+ /**
+ * @var ContentIdentityInterfaceFactory
+ */
+ private $contentIdentityFactory;
+
/**
* @var SynchronizeInterface
*/
private $synchronize;
/**
+ * @var SynchronizeIdentitiesInterface
+ */
+ private $synchronizeIdentities;
+
+ /**
+ * @param SerializerInterface $serializer
+ * @param ContentIdentityInterfaceFactory $contentIdentityFactory
* @param SynchronizeInterface $synchronize
+ * @param SynchronizeIdentitiesInterface $synchronizeIdentities
*/
- public function __construct(SynchronizeInterface $synchronize)
- {
+ public function __construct(
+ SerializerInterface $serializer,
+ ContentIdentityInterfaceFactory $contentIdentityFactory,
+ SynchronizeInterface $synchronize,
+ SynchronizeIdentitiesInterface $synchronizeIdentities
+ ) {
+ $this->serializer = $serializer;
+ $this->contentIdentityFactory = $contentIdentityFactory;
$this->synchronize = $synchronize;
+ $this->synchronizeIdentities = $synchronizeIdentities;
}
/**
* Run media files synchronization.
+ *
+ * @param OperationInterface $operation
+ * @throws LocalizedException
*/
- public function execute() : void
+ public function execute(OperationInterface $operation) : void
{
- $this->synchronize->execute();
+ $identities = $this->serializer->unserialize($operation->getSerializedData());
+
+ if (empty($identities)) {
+ $this->synchronize->execute();
+ return;
+ }
+
+ $contentIdentities = [];
+ foreach ($identities as $identity) {
+ $contentIdentities[] = $this->contentIdentityFactory->create(
+ [
+ self::ENTITY_TYPE => $identity[self::ENTITY_TYPE],
+ self::ENTITY_ID => $identity[self::ENTITY_ID],
+ self::FIELD => $identity[self::FIELD]
+ ]
+ );
+ }
+ $this->synchronizeIdentities->execute($contentIdentities);
}
}
diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php
index ad6fdd27d7067..d9e89fea7d4d2 100644
--- a/app/code/Magento/MediaContentSynchronization/Model/Publish.php
+++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php
@@ -7,7 +7,11 @@
namespace Magento\MediaContentSynchronization\Model;
+use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory;
+use Magento\Framework\Bulk\OperationInterface;
+use Magento\Framework\DataObject\IdentityGeneratorInterface;
use Magento\Framework\MessageQueue\PublisherInterface;
+use Magento\Framework\Serialize\SerializerInterface;
/**
* Publish media content synchronization queue.
@@ -19,27 +23,64 @@ class Publish
*/
private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization';
+ /**
+ * @var OperationInterfaceFactory
+ */
+ private $operationFactory;
+
+ /**
+ * @var IdentityGeneratorInterface
+ */
+ private $identityService;
+
/**
* @var PublisherInterface
*/
private $publisher;
/**
+ * @var SerializerInterface
+ */
+ private $serializer;
+
+ /**
+ * @param OperationInterfaceFactory $operationFactory
+ * @param IdentityGeneratorInterface $identityService
* @param PublisherInterface $publisher
+ * @param SerializerInterface $serializer
*/
- public function __construct(PublisherInterface $publisher)
- {
+ public function __construct(
+ OperationInterfaceFactory $operationFactory,
+ IdentityGeneratorInterface $identityService,
+ PublisherInterface $publisher,
+ SerializerInterface $serializer
+ ) {
+ $this->operationFactory = $operationFactory;
+ $this->identityService = $identityService;
+ $this->serializer = $serializer;
$this->publisher = $publisher;
}
/**
- * Publish media content synchronization message to the message queue.
+ * Publish media content synchronization message to the message queue
+ *
+ * @param array $contentIdentities
*/
- public function execute() : void
+ public function execute(array $contentIdentities = []) : void
{
+ $data = [
+ 'data' => [
+ 'bulk_uuid' => $this->identityService->generateId(),
+ 'topic_name' => self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION,
+ 'serialized_data' => $this->serializer->serialize($contentIdentities),
+ 'status' => OperationInterface::STATUS_TYPE_OPEN,
+ ]
+ ];
+ $operation = $this->operationFactory->create($data);
+
$this->publisher->publish(
self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION,
- [self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION]
+ $operation
);
}
}
diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php
new file mode 100644
index 0000000000000..1bf57c6b2ec42
--- /dev/null
+++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php
@@ -0,0 +1,71 @@
+log = $log;
+ $this->synchronizeIdentitiesPool = $synchronizeIdentitiesPool;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(array $mediaContentIdentities): void
+ {
+ $failed = [];
+
+ foreach ($this->synchronizeIdentitiesPool->get() as $name => $synchronizer) {
+ try {
+ $synchronizer->execute($mediaContentIdentities);
+ } catch (\Exception $exception) {
+ $this->log->critical($exception);
+ $failed[] = $name;
+ }
+ }
+
+ if (!empty($failed)) {
+ throw new LocalizedException(
+ __(
+ 'Failed to execute the following content synchronizers: %synchronizers',
+ [
+ 'synchronizers' => implode(', ', $failed)
+ ]
+ )
+ );
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php
new file mode 100644
index 0000000000000..2314796481b55
--- /dev/null
+++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php
@@ -0,0 +1,124 @@
+consumerFactory = Bootstrap::getObjectManager()->get(ConsumerFactory::class);
+ $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class);
+ $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class);
+ $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class);
+ $this->publish = Bootstrap::getObjectManager()->get(Publish::class);
+ }
+
+ /**
+ * @dataProvider filesProvider
+ * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php
+ * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php
+ * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php
+ * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php
+ * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php
+ * @param array $contentIdentities
+ * @throws IntegrationException
+ * @throws LocalizedException
+ */
+ public function testExecute(array $contentIdentities): void
+ {
+ // publish message to the queue
+ $this->publish->execute($contentIdentities);
+
+ // run and process message
+ $batchSize = 1;
+ $maxNumberOfMessages = 1;
+ $consumer = $this->consumerFactory->get(self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, $batchSize);
+ $consumer->process($maxNumberOfMessages);
+
+ // verify synchronized media content
+ $assetId = 2020;
+ $entityIds = [];
+ foreach ($contentIdentities as $contentIdentity) {
+ $contentIdentityObject = $this->contentIdentityFactory->create($contentIdentity);
+ $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentityObject));
+ $entityIds[] = $contentIdentityObject->getEntityId();
+ }
+
+ $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]);
+ $this->assertEquals(2, count($synchronizedContentIdentities));
+
+ foreach ($synchronizedContentIdentities as $syncedContentIdentity) {
+ $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds);
+ }
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array
+ */
+ public function filesProvider(): array
+ {
+ return [
+ [
+ [
+ [
+ 'entityType' => 'catalog_category',
+ 'field' => 'description',
+ 'entityId' => 28767
+ ],
+ [
+ 'entityType' => 'catalog_product',
+ 'field' => 'description',
+ 'entityId' => 1567
+ ]
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json
index 3be5f535487ec..9f0f4f9588ad6 100644
--- a/app/code/Magento/MediaContentSynchronization/composer.json
+++ b/app/code/Magento/MediaContentSynchronization/composer.json
@@ -4,9 +4,10 @@
"require": {
"php": "~7.3.0||~7.4.0",
"magento/framework": "*",
+ "magento/framework-bulk": "*",
"magento/module-media-content-synchronization-api": "*",
- "magento/framework-message-queue": "*",
- "magento/module-media-content-api": "*"
+ "magento/module-media-content-api": "*",
+ "magento/module-asynchronous-operations": "*"
},
"suggest": {
"magento/module-media-gallery-synchronization": "*"
diff --git a/app/code/Magento/MediaContentSynchronization/etc/communication.xml b/app/code/Magento/MediaContentSynchronization/etc/communication.xml
index e3436aee85331..05641b7432564 100644
--- a/app/code/Magento/MediaContentSynchronization/etc/communication.xml
+++ b/app/code/Magento/MediaContentSynchronization/etc/communication.xml
@@ -7,7 +7,7 @@
-->
-
+
diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml
index d4615c15206e5..e5347f1a11561 100644
--- a/app/code/Magento/MediaContentSynchronization/etc/di.xml
+++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml
@@ -7,6 +7,7 @@
-->
+
diff --git a/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php
new file mode 100644
index 0000000000000..7e21cbb570053
--- /dev/null
+++ b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php
@@ -0,0 +1,23 @@
+synchronizers = $synchronizers;
+ }
+
+ /**
+ * Get all synchronizers from the pool
+ *
+ * @return SynchronizeIdentitiesInterface[]
+ */
+ public function get(): array
+ {
+ return $this->synchronizers;
+ }
+}
diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json
index 1f1e5e4b51c5b..398aaf1de8071 100644
--- a/app/code/Magento/MediaContentSynchronizationApi/composer.json
+++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json
@@ -3,7 +3,8 @@
"description": "Magento module responsible for the media content synchronization implementation API",
"require": {
"php": "~7.3.0||~7.4.0",
- "magento/framework": "*"
+ "magento/framework": "*",
+ "magento/module-media-content-api": "*"
},
"type": "magento2-module",
"license": [
diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php
new file mode 100644
index 0000000000000..77188b65a8b88
--- /dev/null
+++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php
@@ -0,0 +1,57 @@
+updateContentAssetLinks = $updateContentAssetLinks;
+ $this->getEntityContents = $getEntityContents;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $mediaContentIdentities): void
+ {
+ foreach ($mediaContentIdentities as $identity) {
+ if ($identity->getEntityType() === self::FIELD_CATALOG_PRODUCT
+ || $identity->getEntityType() === self::FIELD_CATALOG_CATEGORY
+ ) {
+ $this->updateContentAssetLinks->execute(
+ $identity,
+ implode(PHP_EOL, $this->getEntityContents->execute($identity))
+ );
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php
new file mode 100644
index 0000000000000..5be72e2b4bf60
--- /dev/null
+++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php
@@ -0,0 +1,121 @@
+contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class);
+ $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class);
+ $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class);
+ $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class);
+ }
+
+ /**
+ * @dataProvider filesProvider
+ * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php
+ * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php
+ * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php
+ * @param ContentIdentityInterface[] $mediaContentIdentities
+ * @throws IntegrationException
+ */
+ public function testExecute(array $mediaContentIdentities): void
+ {
+ $assetId = 2020;
+
+ $contentIdentities = [];
+ foreach ($mediaContentIdentities as $mediaContentIdentity) {
+ $contentIdentities[] = $this->contentIdentityFactory->create(
+ [
+ self::ENTITY_TYPE => $mediaContentIdentity[self::ENTITY_TYPE],
+ self::ENTITY_ID => $mediaContentIdentity[self::ENTITY_ID],
+ self::FIELD => $mediaContentIdentity[self::FIELD]
+ ]
+ );
+ }
+
+ $this->assertNotEmpty($contentIdentities);
+ $this->assertEmpty($this->getContentIdentities->execute([$assetId]));
+ $this->synchronizeIdentities->execute($contentIdentities);
+
+ $entityIds = [];
+ foreach ($contentIdentities as $contentIdentity) {
+ $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity));
+ $entityIds[] = $contentIdentity->getEntityId();
+ }
+
+ $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]);
+ $this->assertEquals(2, count($synchronizedContentIdentities));
+
+ foreach ($synchronizedContentIdentities as $syncedContentIdentity) {
+ $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds);
+ }
+ }
+
+ /**
+ * Data provider
+ *
+ * @return array
+ */
+ public function filesProvider(): array
+ {
+ return [
+ [
+ [
+ [
+ 'entityType' => 'catalog_category',
+ 'field' => 'description',
+ 'entityId' => 28767
+ ],
+ [
+ 'entityType' => 'catalog_product',
+ 'field' => 'description',
+ 'entityId' => 1567
+ ]
+ ]
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml
index 8cc86fde8fbcd..070f25f501712 100644
--- a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml
+++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml
@@ -38,4 +38,13 @@
+
+
+
+ - Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\SynchronizeIdentities
+
+
+
+
diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php
new file mode 100644
index 0000000000000..7dd2596a910de
--- /dev/null
+++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php
@@ -0,0 +1,90 @@
+resourceConnection = $resourceConnection;
+ $this->updateContentAssetLinks = $updateContentAssetLinks;
+ $this->getEntityContents = $getEntityContents;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(array $mediaContentIdentities): void
+ {
+ foreach ($mediaContentIdentities as $identity) {
+ if ($identity->getEntityType() === self::FIELD_CMS_PAGE
+ || $identity->getEntityType() === self::FIELD_CMS_BLOCK
+ ) {
+ $this->updateContentAssetLinks->execute(
+ $identity,
+ $this->getCmsMediaContent($identity->getEntityType(), (int)$identity->getEntityId())
+ );
+ }
+ }
+ }
+
+ /**
+ * Get cms media content from database
+ *
+ * @param string $tableName
+ * @param int $cmsId
+ * @return string
+ */
+ private function getCmsMediaContent(string $tableName, int $cmsId): string
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $tableName = $this->resourceConnection->getTableName($tableName);
+ $idField = $tableName == self::FIELD_CMS_BLOCK ? $idField = self::ID_CMS_BLOCK : self::ID_CMS_PAGE;
+
+ $select = $connection->select()
+ ->from($tableName, self::COLUMN_CMS_CONTENT)
+ ->where($idField . '= ?', $cmsId);
+ $data = $connection->fetchOne($select);
+
+ return (string)$data;
+ }
+}
diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php
new file mode 100644
index 0000000000000..825542baaff8c
--- /dev/null
+++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php
@@ -0,0 +1,158 @@
+contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class);
+ $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class);
+ $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class);
+ $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php
+ * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php
+ * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php
+ * @throws IntegrationException
+ * @throws LocalizedException
+ */
+ public function testExecute(): void
+ {
+ $assetId = 2020;
+ $pageId = $this->getPage('fixture_page_with_asset')->getId();
+ $blockId = $this->getBlock('fixture_block_with_asset')->getId();
+ $mediaContentIdentities = [
+ [
+ 'entityType' => 'cms_page',
+ 'field' => 'content',
+ 'entityId' => $pageId
+ ],
+ [
+ 'entityType' => 'cms_block',
+ 'field' => 'content',
+ 'entityId' => $blockId
+ ]
+ ];
+
+ $contentIdentities = [];
+ foreach ($mediaContentIdentities as $mediaContentIdentity) {
+ $contentIdentities[] = $this->contentIdentityFactory->create(
+ [
+ self::ENTITY_TYPE => $mediaContentIdentity[self::ENTITY_TYPE],
+ self::ENTITY_ID => $mediaContentIdentity[self::ENTITY_ID],
+ self::FIELD => $mediaContentIdentity[self::FIELD]
+ ]
+ );
+ }
+
+ $this->assertNotEmpty($contentIdentities);
+ $this->assertEmpty($this->getContentIdentities->execute([$assetId]));
+ $this->synchronizeIdentities->execute($contentIdentities);
+
+ $entityIds = [];
+ foreach ($contentIdentities as $contentIdentity) {
+ $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity));
+ $entityIds[] = $contentIdentity->getEntityId();
+ }
+
+ $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]);
+ $this->assertEquals(2, count($synchronizedContentIdentities));
+
+ foreach ($synchronizedContentIdentities as $syncedContentIdentity) {
+ $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds);
+ }
+ }
+
+ /**
+ * Get fixture block
+ *
+ * @param string $identifier
+ * @return BlockInterface
+ * @throws LocalizedException
+ */
+ private function getBlock(string $identifier): BlockInterface
+ {
+ $objectManager = Bootstrap::getObjectManager();
+
+ /** @var BlockRepositoryInterface $blockRepository */
+ $blockRepository = $objectManager->get(BlockRepositoryInterface::class);
+
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, $identifier)
+ ->create();
+
+ return current($blockRepository->getList($searchCriteria)->getItems());
+ }
+
+ /**
+ * Get fixture page
+ *
+ * @param string $identifier
+ * @return PageInterface
+ * @throws LocalizedException
+ */
+ private function getPage(string $identifier): PageInterface
+ {
+ $objectManager = Bootstrap::getObjectManager();
+
+ /** @var PageRepositoryInterface $repository */
+ $repository = $objectManager->get(PageRepositoryInterface::class);
+
+ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
+ $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
+ $searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, $identifier)
+ ->create();
+
+ return current($repository->getList($searchCriteria)->getItems());
+ }
+}
diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml
index 7def330298789..d6e7604c71d97 100644
--- a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml
+++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml
@@ -14,6 +14,15 @@
+
+
+
+ - Magento\MediaContentSynchronizationCms\Model\Synchronizer\SynchronizeIdentities
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php
index d439b53c120cb..b683ec8fe9d91 100644
--- a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php
+++ b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php
@@ -81,17 +81,18 @@ public function __construct(
*
* @param ImageUploader $subject
* @param string $imagePath
+ * @param string $initialImageName
* @return string
* @throws LocalizedException
*/
- public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath): string
+ public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath, string $initialImageName): string
{
if (!$this->config->isEnabled()) {
return $imagePath;
}
$absolutePath = $this->storage->getCmsWysiwygImages()->getStorageRoot() . $imagePath;
- $tmpPath = $subject->getBaseTmpPath() . '/' . substr(strrchr($imagePath, '/'), 1);
+ $tmpPath = $subject->getBaseTmpPath() . '/' . $initialImageName;
$tmpAssets = $this->getAssetsByPaths->execute([$tmpPath]);
if (!empty($tmpAssets)) {
diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml
new file mode 100644
index 0000000000000..8add2021f056b
--- /dev/null
+++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml
index de8517eedae0e..5a375d9153a6d 100644
--- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml
+++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml
@@ -9,6 +9,9 @@
+
+
+
@@ -21,10 +24,6 @@
-
-
-
-
@@ -37,9 +36,13 @@
-
-
+
+
+
+
+
+
@@ -48,13 +51,11 @@
-
+
-
-
@@ -62,7 +63,7 @@
-
+
diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml
index 038f1ae077b4a..e72e65cf8de90 100644
--- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml
+++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml
@@ -9,6 +9,9 @@
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt
new file mode 100644
index 0000000000000..36b2459f6aa63
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt
@@ -0,0 +1,48 @@
+
+Open Software License ("OSL") v. 3.0
+
+This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
+
+Licensed under the Open Software License version 3.0
+
+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
+
+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
+
+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
+
+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License;
+
+ 4. to perform the Original Work publicly; and
+
+ 5. to display the Original Work publicly.
+
+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
+
+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
+
+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
+
+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
+
+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
+
+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
+
+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
+
+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
+
+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
+
+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
+
+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
+
+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
+
+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
+
+ 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+Academic Free License ("AFL") v. 3.0
+
+This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
+
+Licensed under the Academic Free License version 3.0
+
+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
+
+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
+
+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
+
+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License;
+
+ 4. to perform the Original Work publicly; and
+
+ 5. to display the Original Work publicly.
+
+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
+
+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
+
+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
+
+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
+
+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
+
+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
+
+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
+
+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
+
+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
+
+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
+
+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
+
+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
+
+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
+
+ 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Config.php b/app/code/Magento/MediaGalleryRenditions/Model/Config.php
new file mode 100644
index 0000000000000..d1a48904d1f13
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Model/Config.php
@@ -0,0 +1,118 @@
+scopeConfig = $scopeConfig;
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Check if the media gallery is enabled
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED);
+ }
+
+ /**
+ * Get max width
+ *
+ * @return int
+ */
+ public function getWidth(): int
+ {
+ try {
+ return $this->getDatabaseValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH);
+ } catch (NoSuchEntityException $exception) {
+ return (int) $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH);
+ }
+ }
+
+ /**
+ * Get max height
+ *
+ * @return int
+ */
+ public function getHeight(): int
+ {
+ try {
+ return $this->getDatabaseValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH);
+ } catch (NoSuchEntityException $exception) {
+ return (int) $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH);
+ }
+ }
+
+ /**
+ * Get value from database bypassing config cache
+ *
+ * @param string $path
+ * @return int
+ * @throws NoSuchEntityException
+ */
+ private function getDatabaseValue(string $path): int
+ {
+ $connection = $this->resourceConnection->getConnection();
+ $select = $connection->select()
+ ->from(
+ [
+ 'config' => $this->resourceConnection->getTableName(self::TABLE_CORE_CONFIG_DATA)
+ ],
+ [
+ 'value'
+ ]
+ )
+ ->where('config.path = ?', $path);
+ $value = $connection->query($select)->fetchColumn();
+
+ if ($value === false) {
+ throw new NoSuchEntityException(
+ __(
+ 'The config value for %path is not saved to database.',
+ ['path' => $path]
+ )
+ );
+ }
+
+ return (int) $value;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php
new file mode 100644
index 0000000000000..6bc54fdf9aca4
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php
@@ -0,0 +1,222 @@
+imageFactory = $imageFactory;
+ $this->config = $config;
+ $this->getRenditionPath = $getRenditionPath;
+ $this->filesystem = $filesystem;
+ $this->driver = $driver;
+ $this->isPathExcluded = $isPathExcluded;
+ $this->log = $log;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(array $paths): void
+ {
+ $failedPaths = [];
+
+ foreach ($paths as $path) {
+ try {
+ $this->generateRendition($path);
+ } catch (\Exception $exception) {
+ $this->log->error($exception);
+ $failedPaths[] = $path;
+ }
+ }
+
+ if (!empty($failedPaths)) {
+ throw new LocalizedException(
+ __(
+ 'Cannot create rendition for media asset paths: %paths',
+ [
+ 'paths' => implode(', ', $failedPaths)
+ ]
+ )
+ );
+ }
+ }
+
+ /**
+ * Generate rendition for media asset path
+ *
+ * @param string $path
+ * @throws FileSystemException
+ * @throws LocalizedException
+ * @throws \Exception
+ */
+ private function generateRendition(string $path): void
+ {
+ $this->validateAsset($path);
+
+ $renditionPath = $this->getRenditionPath->execute($path);
+ $this->createDirectory($renditionPath);
+
+ $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path);
+
+ if ($this->shouldFileBeResized($absolutePath)) {
+ $this->createResizedRendition(
+ $absolutePath,
+ $this->getMediaDirectory()->getAbsolutePath($renditionPath)
+ );
+ } else {
+ $this->getMediaDirectory()->copyFile($path, $renditionPath);
+ }
+ }
+
+ /**
+ * Ensure valid media asset path is provided for renditions generation
+ *
+ * @param string $path
+ * @throws FileSystemException
+ * @throws LocalizedException
+ */
+ private function validateAsset(string $path): void
+ {
+ if (!$this->getMediaDirectory()->isFile($path)) {
+ throw new LocalizedException(__('Media asset file %path does not exist!', ['path' => $path]));
+ }
+
+ if ($this->isPathExcluded->execute($path)) {
+ throw new LocalizedException(
+ __('Could not create rendition for image, path is restricted: %path', ['path' => $path])
+ );
+ }
+
+ if (!preg_match(self::IMAGE_FILE_NAME_PATTERN, $path)) {
+ throw new LocalizedException(
+ __('Could not create rendition for image, unsupported file type: %path.', ['path' => $path])
+ );
+ }
+ }
+
+ /**
+ * Create directory for rendition file
+ *
+ * @param string $path
+ * @throws LocalizedException
+ */
+ private function createDirectory(string $path): void
+ {
+ try {
+ $this->getMediaDirectory()->create($this->driver->getParentDirectory($path));
+ } catch (\Exception $exception) {
+ throw new LocalizedException(__('Cannot create directory for rendition %path', ['path' => $path]));
+ }
+ }
+
+ /**
+ * Create rendition file
+ *
+ * @param string $absolutePath
+ * @param string $absoluteRenditionPath
+ * @throws \Exception
+ */
+ private function createResizedRendition(string $absolutePath, string $absoluteRenditionPath): void
+ {
+ $image = $this->imageFactory->create();
+ $image->open($absolutePath);
+ $image->keepAspectRatio(true);
+ $image->resize($this->config->getWidth(), $this->config->getHeight());
+ $image->save($absoluteRenditionPath);
+ }
+
+ /**
+ * Check if image needs to resize or not
+ *
+ * @param string $absolutePath
+ * @return bool
+ */
+ private function shouldFileBeResized(string $absolutePath): bool
+ {
+ [$width, $height] = getimagesize($absolutePath);
+ return $width > $this->config->getWidth() || $height > $this->config->getHeight();
+ }
+
+ /**
+ * Retrieve a media directory instance with write permissions
+ *
+ * @return WriteInterface
+ * @throws FileSystemException
+ */
+ private function getMediaDirectory(): WriteInterface
+ {
+ return $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php b/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php
new file mode 100644
index 0000000000000..1c93141429ab0
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php
@@ -0,0 +1,26 @@
+log = $log;
+ $this->getFilesIterator = $getFilesIterator;
+ $this->filesystem = $filesystem;
+ $this->batchSize = $batchSize;
+ $this->fileExtensions = $fileExtensions;
+ }
+
+ /**
+ * Return files from files system by provided size of batch
+ */
+ public function execute(): \Traversable
+ {
+ $index = 0;
+ $batch = [];
+ $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
+ $iterator = $this->getFilesIterator->execute(
+ $mediaDirectory->getAbsolutePath(self::RENDITIONS_DIRECTORY_NAME)
+ );
+
+ /** @var \SplFileInfo $file */
+ foreach ($iterator as $file) {
+ $relativePath = $mediaDirectory->getRelativePath($file->getPathName());
+ if (!$this->isApplicable($relativePath)) {
+ continue;
+ }
+
+ $batch[] = $relativePath;
+ if (++$index == $this->batchSize) {
+ yield $batch;
+ $index = 0;
+ $batch = [];
+ }
+ }
+ if (count($batch) > 0) {
+ yield $batch;
+ }
+ }
+
+ /**
+ * Is the path a valid image path
+ *
+ * @param string $path
+ * @return bool
+ */
+ private function isApplicable(string $path): bool
+ {
+ try {
+ return $path && preg_match('#\.(' . implode("|", $this->fileExtensions) . ')$# i', $path);
+ } catch (\Exception $exception) {
+ $this->log->critical($exception);
+ return false;
+ }
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php
new file mode 100644
index 0000000000000..97efcdc81ba50
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php
@@ -0,0 +1,33 @@
+publisher = $publisher;
+ }
+
+ /**
+ * Publish media gallery renditions update message to the queue.
+ *
+ * @param array $paths
+ */
+ public function execute(array $paths = []): void
+ {
+ $this->publisher->publish(
+ self::TOPIC_MEDIA_GALLERY_UPDATE_RENDITIONS,
+ $paths
+ );
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php
new file mode 100644
index 0000000000000..45cea58d05018
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php
@@ -0,0 +1,126 @@
+generateRenditions = $generateRenditions;
+ $this->fetchRenditionPathsBatches = $fetchRenditionPathsBatches;
+ $this->log = $log;
+ }
+
+ /**
+ * Update renditions for given paths, if empty array is provided - all renditions are updated
+ *
+ * @param array $paths
+ * @throws LocalizedException
+ */
+ public function execute(array $paths): void
+ {
+ if (!empty($paths)) {
+ $this->updateRenditions($paths);
+ return;
+ }
+
+ foreach ($this->fetchRenditionPathsBatches->execute() as $renditionPaths) {
+ $this->updateRenditions($renditionPaths);
+ }
+ }
+
+ /**
+ * Update renditions and log exceptions
+ *
+ * @param string[] $renditionPaths
+ */
+ private function updateRenditions(array $renditionPaths): void
+ {
+ try {
+ $this->generateRenditions->execute($this->getAssetPaths($renditionPaths));
+ } catch (LocalizedException $exception) {
+ $this->log->error($exception);
+ }
+ }
+
+ /**
+ * Get asset paths based on rendition paths
+ *
+ * @param string[] $renditionPaths
+ * @return string[]
+ */
+ private function getAssetPaths(array $renditionPaths): array
+ {
+ $paths = [];
+
+ foreach ($renditionPaths as $renditionPath) {
+ try {
+ $paths[] = $this->getAssetPath($renditionPath);
+ } catch (\Exception $exception) {
+ $this->log->error($exception);
+ }
+ }
+
+ return $paths;
+ }
+
+ /**
+ * Get asset path based on rendition path
+ *
+ * @param string $renditionPath
+ * @return string
+ * @throws LocalizedException
+ */
+ private function getAssetPath(string $renditionPath): string
+ {
+ if (strpos($renditionPath, self::RENDITIONS_DIRECTORY_NAME) !== 0) {
+ throw new LocalizedException(
+ __(
+ 'Incorrect rendition path provided for update: %path',
+ [
+ 'path' => $renditionPath
+ ]
+ )
+ );
+ }
+
+ return substr($renditionPath, strlen(self::RENDITIONS_DIRECTORY_NAME));
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php
new file mode 100644
index 0000000000000..f0ba8c3533722
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php
@@ -0,0 +1,84 @@
+getRenditionPath = $getRenditionPath;
+ $this->filesystem = $filesystem;
+ $this->log = $log;
+ }
+
+ /**
+ * Remove renditions when assets are removed
+ *
+ * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths
+ * @param void $result
+ * @param array $paths
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterExecute(
+ DeleteAssetsByPathsInterface $deleteAssetsByPaths,
+ $result,
+ array $paths
+ ): void {
+ $this->removeRenditions($paths);
+ }
+
+ /**
+ * Remove rendition files
+ *
+ * @param array $paths
+ */
+ private function removeRenditions(array $paths): void
+ {
+ foreach ($paths as $path) {
+ try {
+ $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA)->delete(
+ $this->getRenditionPath->execute($path)
+ );
+ } catch (\Exception $exception) {
+ $this->log->error($exception);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php b/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php
new file mode 100644
index 0000000000000..ec2012c528ef1
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php
@@ -0,0 +1,111 @@
+getRenditionPath = $getRenditionPath;
+ $this->generateRenditions = $generateRenditions;
+ $this->imagesHelper = $imagesHelper;
+ $this->config = $config;
+ $this->log = $log;
+ }
+
+ /**
+ * Replace the original asset path with rendition path
+ *
+ * @param GetInsertImageContent $subject
+ * @param string $encodedFilename
+ * @param bool $forceStaticPath
+ * @param bool $renderAsTag
+ * @param int|null $storeId
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforeExecute(
+ GetInsertImageContent $subject,
+ string $encodedFilename,
+ bool $forceStaticPath,
+ bool $renderAsTag,
+ ?int $storeId = null
+ ): array {
+ $arguments = [
+ $encodedFilename,
+ $forceStaticPath,
+ $renderAsTag,
+ $storeId
+ ];
+
+ if (!$this->config->isEnabled()) {
+ return $arguments;
+ }
+
+ $path = $this->imagesHelper->idDecode($encodedFilename);
+
+ try {
+ $this->generateRenditions->execute([$path]);
+ } catch (LocalizedException $exception) {
+ $this->log->error($exception);
+ return $arguments;
+ }
+
+ $arguments[0] = $this->imagesHelper->idEncode($this->getRenditionPath->execute($path));
+
+ return $arguments;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php
new file mode 100644
index 0000000000000..9cf969c16782f
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php
@@ -0,0 +1,62 @@
+scheduleRenditionsUpdate = $scheduleRenditionsUpdate;
+ }
+
+ /**
+ * Update renditions when configuration is changed
+ *
+ * @param Value $config
+ * @param Value $result
+ * @return Value
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterSave(Value $config, Value $result): Value
+ {
+ if ($this->isRenditionsValue($result) && $result->isValueChanged()) {
+ $this->scheduleRenditionsUpdate->execute();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Does configuration value relates to renditions
+ *
+ * @param Value $value
+ * @return bool
+ */
+ private function isRenditionsValue(Value $value): bool
+ {
+ return $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH
+ || $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/README.md b/app/code/Magento/MediaGalleryRenditions/README.md
new file mode 100644
index 0000000000000..df856e8003a84
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/README.md
@@ -0,0 +1,13 @@
+# Magento_MediaGalleryRenditions module
+
+The Magento_MediaGalleryRenditions module implements height and width fields for for media gallery items.
+
+## Extensibility
+
+Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html).
+
+[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module.
+
+## Additional information
+
+For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html).
diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php
new file mode 100644
index 0000000000000..05bb01b9ff433
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php
@@ -0,0 +1,116 @@
+extractAssetsFromContent = Bootstrap::getObjectManager()
+ ->get(ExtractAssetsFromContentInterface::class);
+ }
+
+ /**
+ * Assert rendition urls/path in the content are associated with an asset
+ *
+ * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php
+ *
+ * @dataProvider contentProvider
+ * @param string $content
+ * @param array $assetIds
+ */
+ public function testExecute(string $content, array $assetIds): void
+ {
+ $assets = $this->extractAssetsFromContent->execute($content);
+
+ $extractedAssetIds = [];
+ foreach ($assets as $asset) {
+ $extractedAssetIds[] = $asset->getId();
+ }
+
+ sort($assetIds);
+ sort($extractedAssetIds);
+
+ $this->assertEquals($assetIds, $extractedAssetIds);
+ }
+
+ /**
+ * Data provider for testExecute
+ *
+ * @return array
+ */
+ public function contentProvider()
+ {
+ return [
+ 'Empty Content' => [
+ '',
+ []
+ ],
+ 'No paths in content' => [
+ 'content without paths',
+ []
+ ],
+ 'Relevant rendition path in content' => [
+ 'content {{media url=".renditions/testDirectory/path.jpg"}} content',
+ [
+ 2020
+ ]
+ ],
+ 'Relevant wysiwyg rendition path in content' => [
+ 'content [
+ '/pub/media/.renditions/testDirectory/path.jpg',
+ [
+ 2020
+ ]
+ ],
+ 'Relevant rendition path content' => [
+ '/media/.renditions/testDirectory/path.jpg',
+ [
+ 2020
+ ]
+ ],
+ 'Relevant existing media paths w/o rendition in content' => [
+ 'content {{media url="testDirectory/path.jpg"}} content',
+ [
+ 2020
+ ]
+ ],
+ 'Relevant existing paths w/o rendition in content with pub' => [
+ '/pub/media/testDirectory/path.jpg',
+ [
+ 2020
+ ]
+ ],
+ 'Non-existing rendition paths in content' => [
+ 'content {{media url=".renditions/non-existing-path.png"}} content',
+ []
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php
new file mode 100644
index 0000000000000..9655f3949d404
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php
@@ -0,0 +1,124 @@
+generateRenditions = Bootstrap::getObjectManager()->get(GenerateRenditionsInterface::class);
+ $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class)
+ ->getDirectoryWrite(DirectoryList::MEDIA);
+ $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class);
+ $this->renditionSizeConfig = Bootstrap::getObjectManager()->get(Config::class);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ /** @var WriteInterface $mediaDirectory */
+ $mediaDirectory = Bootstrap::getObjectManager()->get(
+ Filesystem::class
+ )->getDirectoryWrite(
+ DirectoryList::MEDIA
+ );
+ if ($mediaDirectory->isExist($mediaDirectory->getAbsolutePath() . '/.renditions')) {
+ $mediaDirectory->delete($mediaDirectory->getAbsolutePath() . '/.renditions');
+ }
+ }
+
+ /**
+ * @dataProvider renditionsImageProvider
+ *
+ * Test for generation of rendition images.
+ *
+ * @param string $path
+ * @param string $renditionPath
+ * @throws LocalizedException
+ */
+ public function testExecute(string $path, string $renditionPath): void
+ {
+ $this->copyImage($path);
+ $this->generateRenditions->execute([$path]);
+ $expectedRenditionPath = $this->mediaDirectory->getAbsolutePath($renditionPath);
+ list($imageWidth, $imageHeight) = getimagesize($expectedRenditionPath);
+ $this->assertFileExists($expectedRenditionPath);
+ $this->assertLessThanOrEqual(
+ $this->renditionSizeConfig->getWidth(),
+ $imageWidth,
+ 'Generated renditions image width should be less than or equal to configured value'
+ );
+ $this->assertLessThanOrEqual(
+ $this->renditionSizeConfig->getHeight(),
+ $imageHeight,
+ 'Generated renditions image height should be less than or equal to configured value'
+ );
+ }
+
+ /**
+ * @param array $paths
+ * @throws FileSystemException
+ */
+ private function copyImage(string $path): void
+ {
+ $imagePath = realpath(__DIR__ . '/../../_files' . $path);
+ $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($path);
+ $this->driver->copy(
+ $imagePath,
+ $modifiableFilePath
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function renditionsImageProvider(): array
+ {
+ return [
+ 'rendition_image_not_generated' => [
+ 'paths' => '/magento_medium_image.jpg',
+ 'renditionPath' => ".renditions/magento_medium_image.jpg"
+ ],
+ 'rendition_image_generated' => [
+ 'paths' => '/magento_large_image.jpg',
+ 'renditionPath' => ".renditions/magento_large_image.jpg"
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php
new file mode 100644
index 0000000000000..0f8b61147986c
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php
@@ -0,0 +1,77 @@
+getRenditionPath = Bootstrap::getObjectManager()->get(GetRenditionPathInterface::class);
+ $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class)
+ ->getDirectoryWrite(DirectoryList::MEDIA);
+ $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class);
+ }
+
+ /**
+ * @dataProvider getImageProvider
+ *
+ * Test for getting a rendition path.
+ */
+ public function testExecute(string $path, string $expectedRenditionPath): void
+ {
+ $imagePath = realpath(__DIR__ . '/../../_files' . $path);
+ $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($path);
+ $this->driver->copy(
+ $imagePath,
+ $modifiableFilePath
+ );
+ $this->assertEquals($expectedRenditionPath, $this->getRenditionPath->execute($path));
+ }
+
+ /**
+ * @return array
+ */
+ public function getImageProvider(): array
+ {
+ return [
+ 'return_original_path' => [
+ 'path' => '/magento_medium_image.jpg',
+ 'expectedRenditionPath' => '.renditions/magento_medium_image.jpg'
+ ],
+ 'return_rendition_path' => [
+ 'path' => '/magento_large_image.jpg',
+ 'expectedRenditionPath' => '.renditions/magento_large_image.jpg'
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg
new file mode 100644
index 0000000000000..c377daf8fb0b3
Binary files /dev/null and b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg differ
diff --git a/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg
new file mode 100644
index 0000000000000..6dc8cd69e41c1
Binary files /dev/null and b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg differ
diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json
new file mode 100644
index 0000000000000..873e0b4a8c60b
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "magento/module-media-gallery-renditions",
+ "description": "Magento module that implements height and width fields for for media gallery items.",
+ "require": {
+ "php": "~7.3.0||~7.4.0",
+ "magento/framework": "*",
+ "magento/module-media-gallery-renditions-api": "*",
+ "magento/module-media-gallery-api": "*",
+ "magento/framework-message-queue": "*",
+ "magento/module-cms": "*"
+ },
+ "suggest": {
+ "magento/module-media-content-api": "*"
+ },
+ "type": "magento2-module",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\MediaGalleryRenditions\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml
new file mode 100644
index 0000000000000..64f338d53a283
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+ validate-zero-or-greater validate-digits
+
+
+
+ validate-zero-or-greater validate-digits
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/communication.xml b/app/code/Magento/MediaGalleryRenditions/etc/communication.xml
new file mode 100644
index 0000000000000..2c343c4f8086a
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/communication.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml
new file mode 100644
index 0000000000000..58c5aa1f11fd2
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/config.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ 1000
+ 1000
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/di.xml b/app/code/Magento/MediaGalleryRenditions/etc/di.xml
new file mode 100644
index 0000000000000..af53810b7f69e
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/di.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+ 100
+
+ - jpg
+ - jpeg
+ - gif
+ - png
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml
new file mode 100644
index 0000000000000..a1fbe5cba558e
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ /{{media url=(?:"|")(?:.renditions)?(.*?)(?:"|")}}/
+ /{{media url="?((?!.*.renditions).*?)"?}}/
+ /src=".*\/media\/(?:.renditions\/)*(.*?)"/
+ /^\/?media\/(?:.renditions\/)?(.*)/
+ /^\/pub\/?media\/(?:.renditions\/)?(.*)/
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/module.xml b/app/code/Magento/MediaGalleryRenditions/etc/module.xml
new file mode 100644
index 0000000000000..93bc9f1c214e6
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/module.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml
new file mode 100644
index 0000000000000..0c584ac12f898
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml
new file mode 100644
index 0000000000000..9618329895230
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml
new file mode 100644
index 0000000000000..260e9f5f7f371
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditions/registration.php b/app/code/Magento/MediaGalleryRenditions/registration.php
new file mode 100644
index 0000000000000..275c06f752a63
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditions/registration.php
@@ -0,0 +1,14 @@
+" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt
new file mode 100644
index 0000000000000..f39d641b18a19
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt
@@ -0,0 +1,48 @@
+
+Academic Free License ("AFL") v. 3.0
+
+This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
+
+Licensed under the Academic Free License version 3.0
+
+ 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
+
+ 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
+
+ 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
+
+ 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License;
+
+ 4. to perform the Original Work publicly; and
+
+ 5. to display the Original Work publicly.
+
+ 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
+
+ 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
+
+ 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
+
+ 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
+
+ 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
+
+ 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
+
+ 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
+
+ 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
+
+ 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
+
+ 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
+
+ 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
+
+ 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
+
+ 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
+
+ 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
diff --git a/app/code/Magento/MediaGalleryRenditionsApi/README.md b/app/code/Magento/MediaGalleryRenditionsApi/README.md
new file mode 100644
index 0000000000000..42478c0c9b520
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditionsApi/README.md
@@ -0,0 +1,13 @@
+# Magento_MediaGalleryRenditionsApi module
+
+The Magento_MediaGalleryRenditionsApi module is responsible for the API implementation of Media Gallery Renditions.
+
+## Extensibility
+
+Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html).
+
+[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditionsApi module.
+
+## Additional information
+
+For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html).
diff --git a/app/code/Magento/MediaGalleryRenditionsApi/composer.json b/app/code/Magento/MediaGalleryRenditionsApi/composer.json
new file mode 100644
index 0000000000000..6e3c559f001c1
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditionsApi/composer.json
@@ -0,0 +1,21 @@
+{
+ "name": "magento/module-media-gallery-renditions-api",
+ "description": "Magento module that is responsible for the API implementation of Media Gallery Renditions.",
+ "require": {
+ "php": "~7.3.0||~7.4.0",
+ "magento/framework": "*"
+ },
+ "type": "magento2-module",
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\MediaGalleryRenditionsApi\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml
new file mode 100644
index 0000000000000..f3a3f87b61105
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryRenditionsApi/registration.php b/app/code/Magento/MediaGalleryRenditionsApi/registration.php
new file mode 100644
index 0000000000000..bf057f2d2adbf
--- /dev/null
+++ b/app/code/Magento/MediaGalleryRenditionsApi/registration.php
@@ -0,0 +1,14 @@
+synchronize = $synchronize;
+ $this->synchronizeFiles = $synchronizeFiles;
}
/**
* Run media files synchronization.
+ *
+ * @param array $paths
+ * @throws LocalizedException
*/
- public function execute() : void
+ public function execute(array $paths) : void
{
- $this->synchronize->execute();
+ if (!empty($paths)) {
+ $this->synchronizeFiles->execute($paths);
+ } else {
+ $this->synchronize->execute();
+ }
}
}
diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php
index 386798d68d9df..ec314416e36ee 100644
--- a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php
+++ b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php
@@ -33,13 +33,15 @@ public function __construct(PublisherInterface $publisher)
}
/**
- * Publish media content synchronization message to the message queue.
+ * Publish media content synchronization message to the message queue
+ *
+ * @param array $paths
*/
- public function execute() : void
+ public function execute(array $paths = []) : void
{
$this->publisher->publish(
self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION,
- [self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION]
+ $paths
);
}
}
diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php
new file mode 100644
index 0000000000000..d797acedda6ec
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php
@@ -0,0 +1,99 @@
+authorization = $authorization;
+ $this->json = $json;
+ parent::__construct($context, $data, $jsonHelper, $directoryHelper);
+ }
+
+ /**
+ * Retrieve actions json
+ *
+ * @return string
+ */
+ public function getActionsJson(): string
+ {
+ $actions = [
+ [
+ 'title' => __('Cancel'),
+ 'handler' => 'closeModal',
+ 'name' => 'cancel',
+ 'classes' => 'action-default scalable cancel action-quaternary'
+ ]
+ ];
+
+ if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) {
+ $actions[] = [
+ 'title' => __('Delete Image'),
+ 'handler' => 'deleteImageAction',
+ 'name' => 'delete',
+ 'classes' => 'action-default scalable delete action-quaternary'
+ ];
+ }
+
+ if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) {
+ $actions[] = [
+ 'title' => __('Edit Details'),
+ 'handler' => 'editImageAction',
+ 'name' => 'edit',
+ 'classes' => 'action-default scalable edit action-quaternary'
+ ];
+ }
+
+ if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::insert_assets')) {
+ $actions[] = [
+ 'title' => __('Add Image'),
+ 'handler' => 'addImage',
+ 'name' => 'add-image',
+ 'classes' => 'scalable action-primary add-image-action'
+ ];
+ }
+
+ return $this->json->serialize($actions);
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php
new file mode 100644
index 0000000000000..7e73b1682f79a
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php
@@ -0,0 +1,90 @@
+authorization = $authorization;
+ $this->json = $json;
+ parent::__construct($context, $data, $jsonHelper, $directoryHelper);
+ }
+
+ /**
+ * Retrieve actions json
+ *
+ * @return string
+ */
+ public function getActionsJson(): string
+ {
+ $standaloneActions = [
+ [
+ 'title' => __('Cancel'),
+ 'handler' => 'closeModal',
+ 'name' => 'cancel',
+ 'classes' => 'action-default scalable cancel action-quaternary'
+ ]
+ ];
+
+ if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) {
+ $standaloneActions[] = [
+ 'title' => __('Delete Image'),
+ 'handler' => 'deleteImageAction',
+ 'name' => 'delete',
+ 'classes' => 'action-default scalable delete action-quaternary'
+ ];
+ }
+
+ if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) {
+ $standaloneActions[] = [
+ 'title' => __('Edit Details'),
+ 'handler' => 'editImageAction',
+ 'name' => 'edit',
+ 'classes' => 'action-default scalable edit action-quaternary'
+ ];
+ }
+
+ return $this->json->serialize($standaloneActions);
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php
index 3d4af88e4ad67..76c00927b33e0 100644
--- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php
+++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php
@@ -29,7 +29,7 @@ class Create extends Action implements HttpPostActionInterface
/**
* @see _isAllowed()
*/
- public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery';
+ public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::create_folder';
/**
* @var CreateDirectoriesByPathsInterface
diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php
index 56f12c5139d65..3dc43e5276860 100644
--- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php
+++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php
@@ -30,7 +30,7 @@ class Delete extends Action implements HttpPostActionInterface
/**
* @see _isAllowed()
*/
- public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery';
+ public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_folder';
/**
* @var DeleteAssetsByPathsInterface
diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php
index a5d1cee7abf41..2f7766c590033 100644
--- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php
+++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php
@@ -31,7 +31,7 @@ class Delete extends Action implements HttpPostActionInterface
/**
* @see _isAllowed()
*/
- public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery';
+ public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_assets';
/**
* @var DeleteImage
diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php
index f41c489607b15..87a2e7345c407 100644
--- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php
+++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php
@@ -32,7 +32,7 @@ class SaveDetails extends Action implements HttpPostActionInterface
/**
* @see _isAllowed()
*/
- public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery';
+ public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::edit_assets';
/**
* @var UpdateAsset
diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php
index e965d94b33f0c..4492595bbe6ee 100644
--- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php
+++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php
@@ -28,7 +28,7 @@ class Upload extends Action implements HttpPostActionInterface
/**
* @see _isAllowed()
*/
- public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery';
+ public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::upload_assets';
/**
* @var UploadImage
diff --git a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php
new file mode 100644
index 0000000000000..e72017e20a7f6
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php
@@ -0,0 +1,112 @@
+moduleDataSetup = $moduleDataSetup;
+ }
+
+ /**
+ * Add child resources permissions for user roles with Magento_Cms::media_gallery permission
+ */
+ public function apply(): void
+ {
+ $tableName = $this->moduleDataSetup->getTable('authorization_rule');
+ $connection = $this->moduleDataSetup->getConnection();
+
+ if (!$tableName) {
+ return;
+ }
+
+ $select = $connection->select()
+ ->from($tableName, ['role_id'])
+ ->where('resource_id = "Magento_Cms::media_gallery"');
+
+ $insertData = $this->getInsertData($connection->fetchCol($select));
+
+ if (!empty($insertData)) {
+ $connection->insertMultiple($tableName, $insertData);
+ }
+ }
+
+ /**
+ * Retrieve data to insert to authorization_rule table based on role ids
+ *
+ * @param array $roleIds
+ * @return array
+ */
+ private function getInsertData(array $roleIds): array
+ {
+ $newResources = [
+ 'Magento_MediaGalleryUiApi::insert_assets',
+ 'Magento_MediaGalleryUiApi::upload_assets',
+ 'Magento_MediaGalleryUiApi::edit_assets',
+ 'Magento_MediaGalleryUiApi::delete_assets',
+ 'Magento_MediaGalleryUiApi::create_folder',
+ 'Magento_MediaGalleryUiApi::delete_folder'
+ ];
+
+ $data = [];
+
+ foreach ($roleIds as $roleId) {
+ foreach ($newResources as $resourceId) {
+ $data[] = [
+ 'role_id' => $roleId,
+ 'resource_id' => $resourceId,
+ 'permission' => 'allow'
+ ];
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAliases(): array
+ {
+ return [];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public static function getDependencies(): array
+ {
+ return [];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public static function getVersion(): string
+ {
+ return '2.4.2';
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml
new file mode 100644
index 0000000000000..af2b383143f62
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Validates that the provided elemen present on page but have attribute disabled.
+
+
+
+
+
+
+
+
+ verifyDisabledAttribute
+ [{{buttonName}}]
+
+
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml
new file mode 100644
index 0000000000000..c212092b657fd
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Requires select folder in directory tree. Assert that selected folder is empty.
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml
new file mode 100644
index 0000000000000..db9d1853df583
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Expand media gallery tmp folder tree
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml
new file mode 100644
index 0000000000000..49aa45426152c
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml
index 451ef81f0ff9f..53781a65e4898 100644
--- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml
@@ -18,11 +18,11 @@
-
-
-
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml
index 7f9a5aefdf69c..907f2c3116800 100644
--- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml
@@ -12,6 +12,7 @@
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml
similarity index 77%
rename from app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml
rename to app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml
index 943f29d5fa851..f35a32b6d3a37 100644
--- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryGridImagePositionSection.xml
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml
@@ -7,7 +7,8 @@
-->
-
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml
new file mode 100644
index 0000000000000..9738ddedc3cc3
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml
new file mode 100644
index 0000000000000..1d51caf0fc400
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml
new file mode 100644
index 0000000000000..121ad25c93f0d
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml
index 963a0b954e45b..5926b115afccf 100644
--- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml
@@ -9,6 +9,9 @@
+
+
+
@@ -58,8 +61,8 @@
-
+
-
+
diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml
new file mode 100644
index 0000000000000..c8f8655d11edb
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php
new file mode 100644
index 0000000000000..039a1006c79e5
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php
@@ -0,0 +1,52 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getButtonData(): array
+ {
+ $buttonData = [
+ 'label' => __('Create Folder'),
+ 'on_click' => 'jQuery("#create_folder").trigger("create_folder");',
+ 'class' => 'action-default scalable add media-gallery-actions-buttons',
+ 'sort_order' => 10,
+ ];
+
+ if (!$this->authorization->isAllowed(self::ACL_CREATE_FOLDER)) {
+ $buttonData['disabled'] = 'disabled';
+ }
+
+ return $buttonData;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php
new file mode 100644
index 0000000000000..10604d65f768f
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php
@@ -0,0 +1,52 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getButtonData(): array
+ {
+ $buttonData = [
+ 'label' => __('Delete Images...'),
+ 'on_click' => 'jQuery(window).trigger("massAction.MediaGallery")',
+ 'class' => 'action-default scalable add media-gallery-actions-buttons',
+ 'sort_order' => 50,
+ ];
+
+ if (!$this->authorization->isAllowed(self::ACL_DELETE_ASSETS)) {
+ $buttonData['disabled'] = 'disabled';
+ }
+
+ return $buttonData;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php
new file mode 100644
index 0000000000000..cb803c1c663e0
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php
@@ -0,0 +1,52 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getButtonData(): array
+ {
+ $buttonData = [
+ 'label' => __('Delete Folder'),
+ 'disabled' => 'disabled',
+ 'on_click' => 'jQuery("#delete_folder").trigger("delete_folder");',
+ 'class' => 'action-default scalable add media-gallery-actions-buttons',
+ 'sort_order' => 30,
+ ];
+ if (!$this->authorization->isAllowed(self::ACL_DELETE_FOLDER)) {
+ $buttonData['disabled'] = 'disabled';
+ }
+
+ return $buttonData;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php
new file mode 100644
index 0000000000000..6854b79ba2c36
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php
@@ -0,0 +1,52 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getButtonData(): array
+ {
+ $buttonData = [
+ 'label' => __('Add Selected'),
+ 'on_click' => 'return false;',
+ 'class' => 'action-primary no-display media-gallery-add-selected',
+ 'sort_order' => 110,
+ ];
+
+ if (!$this->authorization->isAllowed(self::ACL_INSERT_ASSETS)) {
+ $buttonData['disabled'] = 'disabled';
+ }
+
+ return $buttonData;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php
new file mode 100644
index 0000000000000..32bbdba88a599
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php
@@ -0,0 +1,52 @@
+authorization = $authorization;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getButtonData(): array
+ {
+ $buttonData = [
+ 'label' => __('Upload Image'),
+ 'on_click' => 'jQuery("#image-uploader-input").click();',
+ 'class' => 'action-default scalable add media-gallery-actions-buttons',
+ 'sort_order' => 20,
+ ];
+
+ if (!$this->authorization->isAllowed(self::ACL_UPLOAD_ASSETS)) {
+ $buttonData['disabled'] = 'disabled';
+ }
+
+ return $buttonData;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php
index 269bc1f8bcba7..0ad5ad43f6157 100644
--- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php
+++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php
@@ -10,33 +10,46 @@
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Ui\Component\Container;
+use Magento\Framework\AuthorizationInterface;
/**
* Directories tree component
*/
class DirectoryTree extends Container
{
+ private const ACL_IMAGE_ACTIONS = [
+ 'delete_folder' => 'Magento_MediaGalleryUiApi::delete_folder'
+ ];
+
/**
* @var UrlInterface
*/
private $url;
+ /**
+ * @var AuthorizationInterface
+ */
+ private $authorization;
+
/**
* Constructor
*
* @param ContextInterface $context
* @param UrlInterface $url
+ * @param AuthorizationInterface $authorization
* @param array $components
* @param array $data
*/
public function __construct(
ContextInterface $context,
UrlInterface $url,
+ AuthorizationInterface $authorization,
array $components = [],
array $data = []
) {
parent::__construct($context, $components, $data);
$this->url = $url;
+ $this->authorization = $authorization;
}
/**
@@ -50,6 +63,7 @@ public function prepare(): void
array_replace_recursive(
(array) $this->getData('config'),
[
+ 'allowedActions' => $this->getAllowedActions(),
'getDirectoryTreeUrl' => $this->url->getUrl('media_gallery/directories/gettree'),
'deleteDirectoryUrl' => $this->url->getUrl('media_gallery/directories/delete'),
'createDirectoryUrl' => $this->url->getUrl('media_gallery/directories/create')
@@ -57,4 +71,19 @@ public function prepare(): void
)
);
}
+
+ /**
+ * Return allowed actions for media gallery
+ */
+ private function getAllowedActions(): array
+ {
+ $allowedActions = [];
+ foreach (self::ACL_IMAGE_ACTIONS as $key => $action) {
+ if ($this->authorization->isAllowed($action)) {
+ $allowedActions[] = $key;
+ }
+ }
+
+ return $allowedActions;
+ }
}
diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php
index 481f8ab861f0f..0d48a0d0ff0e1 100644
--- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php
+++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php
@@ -15,12 +15,20 @@
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Ui\Component\Listing\Columns\Column;
+use Magento\Framework\AuthorizationInterface;
/**
* Overlay column
*/
class Url extends Column
{
+ private const ACL_IMAGE_ACTIONS = [
+ 'image-details' => 'Magento_Cms::media_gallery',
+ 'insert' => 'Magento_MediaGalleryUiApi::insert_assets',
+ 'delete' => 'Magento_MediaGalleryUiApi::delete_assets',
+ 'edit' => 'Magento_MediaGalleryUiApi::edit_assets'
+ ];
+
/**
* @var StoreManagerInterface
*/
@@ -41,6 +49,11 @@ class Url extends Column
*/
private $storage;
+ /**
+ * @var AuthorizationInterface
+ */
+ private $authorization;
+
/**
* @param ContextInterface $context
* @param UiComponentFactory $uiComponentFactory
@@ -48,6 +61,7 @@ class Url extends Column
* @param UrlInterface $urlInterface
* @param Images $images
* @param Storage $storage
+ * @param AuthorizationInterface $authorization
* @param array $components
* @param array $data
*/
@@ -58,6 +72,7 @@ public function __construct(
UrlInterface $urlInterface,
Images $images,
Storage $storage,
+ AuthorizationInterface $authorization,
array $components = [],
array $data = []
) {
@@ -66,6 +81,7 @@ public function __construct(
$this->urlInterface = $urlInterface;
$this->images = $images;
$this->storage = $storage;
+ $this->authorization = $authorization;
}
/**
@@ -98,6 +114,7 @@ public function prepare(): void
array_replace_recursive(
(array)$this->getData('config'),
[
+ 'allowedActions' => $this->getAllowedActions(),
'onInsertUrl' => $this->urlInterface->getUrl('cms/wysiwyg_images/oninsert'),
'storeId' => $this->storeManager->getStore()->getId()
]
@@ -105,6 +122,21 @@ public function prepare(): void
);
}
+ /**
+ * Return allowed actions for media gallery image
+ */
+ private function getAllowedActions(): array
+ {
+ $allowedActions = [];
+ foreach (self::ACL_IMAGE_ACTIONS as $key => $action) {
+ if ($this->authorization->isAllowed($action)) {
+ $allowedActions[] = $key;
+ }
+ }
+
+ return $allowedActions;
+ }
+
/**
* Get URL for the provided media asset path
*
diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php
new file mode 100644
index 0000000000000..7d7b67125df96
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php
@@ -0,0 +1,77 @@
+ 'Magento_MediaGalleryUiApi::delete_assets'
+ ];
+
+ /**
+ * @var AuthorizationInterface
+ */
+ private $authorization;
+
+ /**
+ * Constructor
+ *
+ * @param ContextInterface $context
+ * @param AuthorizationInterface $authorization
+ * @param array $components
+ * @param array $data
+ */
+ public function __construct(
+ ContextInterface $context,
+ AuthorizationInterface $authorization,
+ array $components = [],
+ array $data = []
+ ) {
+ parent::__construct($context, $components, $data);
+ $this->authorization = $authorization;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function prepare(): void
+ {
+ parent::prepare();
+ $this->setData(
+ 'config',
+ array_replace_recursive(
+ (array)$this->getData('config'),
+ [
+ 'allowedActions' => $this->getAllowedActions()
+ ]
+ )
+ );
+ }
+
+ /**
+ * Return allowed actions for media gallery
+ */
+ private function getAllowedActions(): array
+ {
+ $allowedActions = [];
+ foreach (self::ACL_IMAGE_ACTIONS as $key => $action) {
+ if ($this->authorization->isAllowed($action)) {
+ $allowedActions[] = $key;
+ }
+ }
+
+ return $allowedActions;
+ }
+}
diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json
index f4701306eb369..204e0b37c3bf8 100644
--- a/app/code/Magento/MediaGalleryUi/composer.json
+++ b/app/code/Magento/MediaGalleryUi/composer.json
@@ -12,7 +12,9 @@
"magento/module-media-gallery-metadata-api": "*",
"magento/module-media-gallery-synchronization-api": "*",
"magento/module-media-content-api": "*",
- "magento/module-cms": "*"
+ "magento/module-cms": "*",
+ "magento/module-directory": "*",
+ "magento/module-authorization": "*"
},
"type": "magento2-module",
"license": [
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml
index f41c0f91b2249..a5eb247bd344f 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml
@@ -16,7 +16,7 @@
-
+
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml
index 7750f22b39ce7..b4f377627c850 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml
@@ -10,7 +10,7 @@
-
+
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml
index a6da20a255192..5df5c1a6c4cbd 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml
@@ -4,11 +4,11 @@
* See COPYING.txt for license details.
*/
-use Magento\Backend\Block\Template;
+use Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails;
use Magento\Framework\Escaper;
// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength
-/** @var Template $block */
+/** @var ImageDetails $block */
/** @var Escaper $escaper */
?>
@@ -73,37 +73,10 @@ use Magento\Framework\Escaper;
"modalWindowSelector": ".media-gallery-image-details",
"imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url",
"mediaGalleryImageDetailsName": "mediaGalleryImageDetails",
- "actionsList": [
- {
- "title": "= $escaper->escapeJs(__('Cancel')); ?>",
- "handler": "closeModal",
- "name": "cancel",
- "classes": "action-default scalable cancel action-quaternary"
- },
- {
- "title": "= $escaper->escapeJs(__('Delete Image')); ?>",
- "handler": "deleteImageAction",
- "name": "delete",
- "classes": "action-default scalable delete action-quaternary"
- },
- {
- "title": "= $escaper->escapeJs(__('Edit Details')); ?>",
- "handler": "editImageAction",
- "name": "edit",
- "classes": "action-default scalable edit action-quaternary"
- },
- {
- "title": "= $escaper->escapeJs(__('Add Image')); ?>",
- "handler": "addImage",
- "name": "add-image",
- "classes": "scalable action-primary add-image-action"
- }
- ]
+ "actionsList": = /* @noEscape */ $block->getActionsJson() ?>
}
}
}
}
}
-
-
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml
index 288a2eaee5221..fdae0a549606c 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml
@@ -4,10 +4,8 @@
* See COPYING.txt for license details.
*/
-use Magento\Backend\Block\Template;
-
// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength
-/** @var Template $block */
+/** @var \Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails $block */
/** @var \Magento\Framework\Escaper $escaper */
?>
@@ -71,31 +69,10 @@ use Magento\Backend\Block\Template;
"modalWindowSelector": ".media-gallery-image-details",
"mediaGalleryImageDetailsName": "mediaGalleryImageDetails",
"imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url",
- "actionsList": [
- {
- "title": "= $escaper->escapeJs(__('Cancel')); ?>",
- "handler": "closeModal",
- "name": "cancel",
- "classes": "action-default scalable cancel action-quaternary"
- },
- {
- "title": "= $escaper->escapeJs(__('Delete Image')); ?>",
- "handler": "deleteImageAction",
- "name": "delete",
- "classes": "action-default scalable delete action-quaternary"
- },
- {
- "title": "= $escaper->escapeJs(__('Edit Details')); ?>",
- "handler": "editImageAction",
- "name": "edit",
- "classes": "action-default scalable edit action-quaternary"
- }
- ]
+ "actionsList": = /* @noEscape */ $block->getActionsJson() ?>
}
}
}
}
}
-
-
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml
index 66731b1cbae6f..b7307f9a74fae 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml
@@ -16,43 +16,17 @@
-
+
-
-
-
-
+
+
+
+
media_gallery_columns
@@ -207,6 +181,7 @@
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml
index 3656a8ea25f74..a53a46c61f75d 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml
@@ -20,30 +20,10 @@
standalone_media_gallery_listing.media_gallery_listing_data_source
-
-
-
-
+
+
+
+
@@ -194,6 +174,7 @@
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less
index 6b3cd610f0348..4b0d8f7dec89e 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less
@@ -99,6 +99,9 @@
.media-gallery-container {
+ .action-disabled {
+ opacity: .5;
+ }
.masonry-image-grid .no-data-message-container,
.masonry-image-grid .error-message-container {
left: 50%;
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js
index 6d8d38a1ca1d6..5555baeabb66a 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js
@@ -19,6 +19,7 @@ define([
return Component.extend({
defaults: {
+ allowedActions: [],
directoryTreeSelector: '#media-gallery-directory-tree',
deleteButtonSelector: '#delete_folder',
createFolderButtonSelector: '#create_folder',
@@ -186,6 +187,10 @@ define([
* @param {String} folderId
*/
setActive: function (folderId) {
+ if (!this.allowedActions.includes('delete_folder')) {
+ return;
+ }
+
this.selectedFolder(folderId);
$(this.deleteButtonSelector).removeAttr('disabled').removeClass('disabled');
}
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js
index 2e1e9a980cd59..84f253da826a8 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js
@@ -17,6 +17,7 @@ define([
return Component.extend({
defaults: {
+ allowedActions: [],
filterChipsProvider: 'componentType = filters, ns = ${ $.ns }',
directoryTreeSelector: '#media-gallery-directory-tree',
getDirectoryTreeUrl: 'media_gallery/directories/gettree',
@@ -32,7 +33,8 @@ define([
},
viewConfig: [{
component: 'Magento_MediaGalleryUi/js/directory/directories',
- name: '${ $.name }_directories'
+ name: '${ $.name }_directories',
+ allowedActions: '${ $.allowedActions }'
}]
},
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js
index bf852d0ddae68..974e22e23737c 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js
@@ -13,10 +13,13 @@ define([
return Column.extend({
defaults: {
bodyTmpl: 'Magento_MediaGalleryUi/grid/columns/image',
+ messageContentSelector: 'ul.messages',
+ mediaGalleryContainerSelector: '.media-gallery-container',
deleteImageUrl: 'media_gallery/image/delete',
addSelectedBtnSelector: '#add_selected',
deleteSelectedBtnSelector: '#delete_selected',
selected: null,
+ allowedActions: [],
fields: {
id: 'id',
url: 'url',
@@ -39,7 +42,8 @@ define([
{
component: 'Magento_MediaGalleryUi/js/grid/columns/image/actions',
name: '${ $.name }_actions',
- imageModelName: '${ $.name }'
+ imageModelName: '${ $.name }',
+ allowedActions: '${ $.allowedActions }'
}
]
},
@@ -222,8 +226,15 @@ define([
toggleAddSelectedButton: function () {
if (this.selected() === null) {
this.hideAddSelectedAndDeleteButon();
- } else {
+
+ return;
+ }
+
+ if (this.allowedActions.includes('insert')) {
$(this.addSelectedBtnSelector).removeClass('no-display');
+ }
+
+ if (this.allowedActions.includes('delete')) {
$(this.deleteSelectedBtnSelector).removeClass('no-display');
}
},
@@ -270,6 +281,7 @@ define([
*/
addMessage: function (code, message) {
this.messages().add(code, message);
+ this.scrollToMessageContent();
this.messages().scheduleCleanup();
},
@@ -284,6 +296,20 @@ define([
!this.massaction().massActionMode()) {
this.deselectImage();
}
+ },
+
+ /**
+ * Scroll to the top of media gallery page
+ */
+ scrollToMessageContent: function () {
+ var scrollTargetElement = $(this.messageContentSelector),
+ scrollTargetContainer = $(this.mediaGalleryContainerSelector);
+
+ scrollTargetContainer.find(scrollTargetElement).get(0).scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'nearest'
+ });
}
});
});
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js
index 38743c8d83d3b..76e051072285a 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js
@@ -8,7 +8,8 @@ define([
'uiComponent',
'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation',
'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction',
- 'mage/translate'
+ 'mage/translate',
+ 'Magento_Ui/js/lib/view/utils/async'
], function ($, _, Component, deleteImageWithDetailConfirmation, image, $t) {
'use strict';
@@ -17,20 +18,24 @@ define([
template: 'Magento_MediaGalleryUi/grid/columns/image/actions',
mediaGalleryImageDetailsName: 'mediaGalleryImageDetails',
mediaGalleryEditDetailsName: 'mediaGalleryEditDetails',
+ allowedActions: [],
actionsList: [
{
name: 'image-details',
title: $t('View Details'),
+ classes: 'action-menu-item',
handler: 'viewImageDetails'
},
{
name: 'edit',
title: $t('Edit'),
+ classes: 'action-menu-item',
handler: 'editImageDetails'
},
{
name: 'delete',
title: $t('Delete'),
+ classes: 'action-menu-item media-gallery-delete-assets',
handler: 'deleteImageAction'
}
],
@@ -50,6 +55,16 @@ define([
this._super();
this.initEvents();
+ this.actionsList = this.actionsList.filter(function (item) {
+ return this.allowedActions.includes(item.name);
+ }.bind(this));
+
+ if (!this.allowedActions.includes('delete')) {
+ $.async('.media-gallery-delete-assets', function () {
+ $('.media-gallery-delete-assets').unbind('click').addClass('action-disabled');
+ });
+ }
+
return this;
},
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js
index 03e82e65b5db5..a20239fb1165e 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js
@@ -16,6 +16,7 @@ define([
return Component.extend({
defaults: {
+ allowedActions: [],
deleteButtonSelector: '#delete_selected_massaction',
deleteImagesSelector: '#delete_massaction',
mediaGalleryImageDetailsName: 'mediaGalleryImageDetails',
@@ -106,6 +107,10 @@ define([
* If images records less than one, disable "delete images" button
*/
checkButtonVisibility: function () {
+ if (!this.allowedActions.includes('delete_assets')) {
+ return;
+ }
+
if (this.imageItems.length < 1) {
$(this.deleteImagesSelector).addClass('disabled');
} else {
diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html
index 042e119b9f40e..72447196cea55 100644
--- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html
+++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html
@@ -7,9 +7,9 @@
-
-
\ No newline at end of file
+
diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json
index f8d5ef11058c1..d577f50523f13 100644
--- a/app/code/Magento/MediaGalleryUiApi/composer.json
+++ b/app/code/Magento/MediaGalleryUiApi/composer.json
@@ -5,6 +5,9 @@
"php": "~7.3.0||~7.4.0",
"magento/framework": "*"
},
+ "suggest": {
+ "magento/module-cms": "*"
+ },
"type": "magento2-module",
"license": [
"OSL-3.0",
diff --git a/app/code/Magento/MediaGalleryUiApi/etc/acl.xml b/app/code/Magento/MediaGalleryUiApi/etc/acl.xml
new file mode 100644
index 0000000000000..c496c57d51322
--- /dev/null
+++ b/app/code/Magento/MediaGalleryUiApi/etc/acl.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php
index a1228903e2323..c19dbc2c429ae 100644
--- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php
+++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php
@@ -119,9 +119,8 @@ public function execute(\Magento\Framework\Event\Observer $observer)
$groupId = null;
if (empty($customerVatNumber) || false == $this->customerVat->isCountryInEU($customerCountryCode)) {
- $groupId = $customer->getId() ? $this->groupManagement->getDefaultGroup(
- $storeId
- )->getId() : $this->groupManagement->getNotLoggedInGroup()->getId();
+ $groupId = $customer->getId() ? $quote->getCustomerGroupId() :
+ $this->groupManagement->getNotLoggedInGroup()->getId();
} else {
// Magento always has to emulate group even if customer uses default billing/shipping address
$groupId = $this->customerVat->getCustomerGroupIdBasedOnVatNumber(
diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php
index 1920b088b1c0e..ae2a4734215ad 100644
--- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php
@@ -13,7 +13,7 @@
use Magento\Customer\Api\Data\CustomerInterfaceFactory;
use Magento\Customer\Api\Data\GroupInterface;
use Magento\Customer\Api\GroupManagementInterface;
-use Magento\Customer\Helper\Address;
+use Magento\Customer\Helper\Address as CustomerAddress;
use Magento\Customer\Model\Session;
use Magento\Customer\Model\Vat;
use Magento\Framework\Event\Observer;
@@ -21,10 +21,11 @@
use Magento\Quote\Api\Data\ShippingAssignmentInterface;
use Magento\Quote\Api\Data\ShippingInterface;
use Magento\Quote\Model\Quote;
+use Magento\Quote\Model\Quote\Address;
use Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver;
use Magento\Quote\Observer\Frontend\Quote\Address\VatValidator;
-use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\MockObject\MockObject;
/**
* Class CollectTotalsTest
@@ -124,7 +125,7 @@ protected function setUp(): void
true,
['getStoreId', 'getCustomAttribute', 'getId', '__wakeup']
);
- $this->customerAddressMock = $this->createMock(Address::class);
+ $this->customerAddressMock = $this->createMock(CustomerAddress::class);
$this->customerVatMock = $this->createMock(Vat::class);
$this->customerDataFactoryMock = $this->getMockBuilder(CustomerInterfaceFactory::class)
->addMethods(['mergeDataObjectWithArray'])
@@ -174,6 +175,7 @@ protected function setUp(): void
$shippingAssignmentMock = $this->getMockForAbstractClass(ShippingAssignmentInterface::class);
$shippingMock = $this->getMockForAbstractClass(ShippingInterface::class);
+
$shippingAssignmentMock->expects($this->once())->method('getShipping')->willReturn($shippingMock);
$shippingMock->expects($this->once())->method('getAddress')->willReturn($this->quoteAddressMock);
@@ -185,7 +187,6 @@ protected function setUp(): void
$this->quoteMock->expects($this->any())
->method('getCustomer')
->willReturn($this->customerMock);
-
$this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class);
$this->customerSession = $this->getMockBuilder(Session::class)
->disableOriginalConstructor()
@@ -266,26 +267,20 @@ public function testDispatchWithDefaultCustomerGroupId()
->willReturn('customerCountryCode');
$this->quoteAddressMock->expects($this->once())->method('getVatId')->willReturn(null);
- $this->quoteMock->expects($this->once())
+ $this->quoteMock->expects($this->exactly(2))
->method('getCustomerGroupId')
->willReturn('customerGroupId');
$this->customerMock->expects($this->once())->method('getId')->willReturn('1');
- $this->groupManagementMock->expects($this->once())
- ->method('getDefaultGroup')
- ->willReturn($this->groupInterfaceMock);
- $this->groupInterfaceMock->expects($this->once())
- ->method('getId')->willReturn('defaultCustomerGroupId');
+
/** Assertions */
$this->quoteAddressMock->expects($this->once())
->method('setPrevQuoteCustomerGroupId')
->with('customerGroupId');
- $this->quoteMock->expects($this->once())->method('setCustomerGroupId')->with('defaultCustomerGroupId');
$this->customerDataFactoryMock->expects($this->any())
->method('create')
->willReturn($this->customerMock);
$this->quoteMock->expects($this->once())->method('setCustomer')->with($this->customerMock);
-
/** SUT execution */
$this->model->execute($this->observerMock);
}
@@ -343,7 +338,7 @@ public function testDispatchWithAddressCustomerVatIdAndCountryId()
$customerVat = "123123123";
$defaultShipping = 1;
- $customerAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class);
+ $customerAddress = $this->createMock(Address::class);
$customerAddress->expects($this->any())
->method("getVatId")
->willReturn($customerVat);
@@ -379,8 +374,8 @@ public function testDispatchWithEmptyShippingAddress()
$customerCountryCode = "DE";
$customerVat = "123123123";
$defaultShipping = 1;
-
$customerAddress = $this->getMockForAbstractClass(AddressInterface::class);
+
$customerAddress->expects($this->once())
->method("getCountryId")
->willReturn($customerCountryCode);
diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php
index 7ad2e5dde2985..e14d8bde6be74 100644
--- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php
+++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php
@@ -110,6 +110,10 @@ private function findRelations(array $products, array $loadAttributes, int $link
//Matching products with related products.
$relationsData = [];
foreach ($relations as $productId => $relatedIds) {
+ //Remove related products that not exist in map list.
+ $relatedIds = array_filter($relatedIds, function ($relatedId) use ($relatedProducts) {
+ return isset($relatedProducts[$relatedId]);
+ });
$relationsData[$productId] = array_map(
function ($id) use ($relatedProducts) {
return $relatedProducts[$id];
diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php
index 2edd76879d8dc..5f739b2595418 100644
--- a/app/code/Magento/Review/Block/Adminhtml/Add.php
+++ b/app/code/Magento/Review/Block/Adminhtml/Add.php
@@ -27,15 +27,10 @@ protected function _construct()
$this->_mode = 'add';
$this->buttonList->update('save', 'label', __('Save Review'));
$this->buttonList->update('save', 'id', 'save_button');
+ $this->buttonList->update('save', 'style', 'display: none;');
$this->buttonList->update('reset', 'id', 'reset_button');
+ $this->buttonList->update('reset', 'style', 'display: none;');
$this->buttonList->update('reset', 'onclick', 'window.review.formReset()');
- $this->_formScripts[] = '
- require(["prototype"], function(){
- toggleParentVis("add_review_form");
- toggleVis("save_button");
- toggleVis("reset_button");
- });
- ';
// @codingStandardsIgnoreStart
$this->_formInitScripts[] = '
require(["jquery","Magento_Review/js/rating","prototype"], function(jQuery, rating){
diff --git a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php
index 04e6343eb43ca..efffa7a02678a 100644
--- a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php
+++ b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php
@@ -5,6 +5,9 @@
*/
namespace Magento\Review\Block\Adminhtml\Add;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\View\Helper\SecureHtmlRenderer;
+
/**
* Adminhtml add product review form
*
@@ -26,6 +29,11 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic
*/
protected $_systemStore;
+ /**
+ * @var SecureHtmlRenderer
+ */
+ private $secureRenderer;
+
/**
* @param \Magento\Backend\Block\Template\Context $context
* @param \Magento\Framework\Registry $registry
@@ -33,6 +41,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic
* @param \Magento\Store\Model\System\Store $systemStore
* @param \Magento\Review\Helper\Data $reviewData
* @param array $data
+ * @param SecureHtmlRenderer|null $htmlRenderer
*/
public function __construct(
\Magento\Backend\Block\Template\Context $context,
@@ -40,10 +49,12 @@ public function __construct(
\Magento\Framework\Data\FormFactory $formFactory,
\Magento\Store\Model\System\Store $systemStore,
\Magento\Review\Helper\Data $reviewData,
- array $data = []
+ array $data = [],
+ ?SecureHtmlRenderer $htmlRenderer = null
) {
$this->_reviewData = $reviewData;
$this->_systemStore = $systemStore;
+ $this->secureRenderer = $htmlRenderer ?: ObjectManager::getInstance()->get(SecureHtmlRenderer::class);
parent::__construct($context, $registry, $formFactory, $data);
}
@@ -59,6 +70,8 @@ protected function _prepareForm()
$form = $this->_formFactory->create();
$fieldset = $form->addFieldset('add_review_form', ['legend' => __('Review Details')]);
+ $beforeHtml = $this->secureRenderer->renderStyleAsTag('display: none;', '#edit_form');
+ $fieldset->setBeforeElementHtml($beforeHtml);
$fieldset->addField('product_name', 'note', ['label' => __('Product'), 'text' => 'product_name']);
diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml
index 92c01cf380746..4d75589c40e9c 100644
--- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml
+++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml
@@ -28,5 +28,6 @@
+
diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml
index b2e84691a45cf..029bcb8abcc25 100644
--- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml
+++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml
@@ -10,15 +10,15 @@
= $block->escapeHtml($_item->getName()) ?>
- getItemOptions()) : ?>
+ getItemOptions()): ?>
-
+
- = $block->escapeHtml($_option['label']) ?>
- getPrintStatus()) : ?>
+ getPrintStatus()): ?>
getFormatedOptionValue($_option) ?>
- >
- = $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?>
-
+ = $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?>
+
- = $block->escapeHtml($_option['label']) ?>
@@ -27,7 +27,7 @@
-
+
-
= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?>
@@ -37,10 +37,10 @@
- getLinks()) : ?>
+ getLinks()): ?>
- = $block->escapeHtml($block->getLinksTitle()) ?>
- getPurchasedItems() as $link) : ?>
+ getPurchasedItems() as $link): ?>
- = $block->escapeHtml($link->getLinkTitle()) ?>
@@ -48,12 +48,14 @@
getProductAdditionalInformationBlock(); ?>
-
+
= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?>
= $block->escapeHtml($_item->getDescription()) ?>
|
- = /* @noEscape */ $block->prepareSku($block->getSku()) ?> |
+
+ = /* @noEscape */ $block->prepareSku($block->getSku()) ?>
+ |
= $block->getItemPriceHtml() ?>
|
@@ -61,7 +63,9 @@
= $block->getItemRowTotalHtml() ?>
|
- = /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?> |
+
+ = /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?>
+ |
= $block->getItemRowTotalAfterDiscountHtml() ?>
|
diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml
index 0176582f0fcd7..d9542d13aba6d 100644
--- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml
+++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml
@@ -10,15 +10,15 @@
= $block->escapeHtml($_item->getName()) ?>
- getItemOptions()) : ?>
+ getItemOptions()): ?>
-
+
- = $block->escapeHtml($_option['label']) ?>
- getPrintStatus()) : ?>
+ getPrintStatus()): ?>
getFormatedOptionValue($_option) ?>
- >
- = $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?>
-
+ = $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?>
+
- = $block->escapeHtml($_option['label']) ?>
@@ -27,19 +27,21 @@
-
+
- = $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?>
getProductAdditionalInformationBlock(); ?>
-
+
= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?>
= $block->escapeHtml($_item->getDescription()) ?>
|
- = /* @noEscape */ $block->prepareSku($block->getSku()) ?> |
+
+ = /* @noEscape */ $block->prepareSku($block->getSku()) ?>
+ |
= $block->getItemPriceHtml() ?>
|
diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml
index 51e43476238be..9cae232ca6541 100644
--- a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml
+++ b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml
@@ -10,15 +10,15 @@ $_item = $block->getItem();
= $block->escapeHtml($_item->getName()) ?>
- getItemOptions()) : ?>
+ getItemOptions()): ?>
-
+
- = $block->escapeHtml($_option['label']) ?>
- getPrintStatus()) : ?>
+ getPrintStatus()): ?>
getFormatedOptionValue($_option) ?>
- >
- = $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?>
-
+ = $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?>
+
- = $block->escapeHtml($_option['label']) ?>
@@ -27,43 +27,46 @@ $_item = $block->getItem();
-
- - = $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?>
+
+
+ - = $block->escapeHtml($optionValue) ?>
getProductAdditionalInformationBlock(); ?>
-
+
= $addtInfoBlock->setItem($_item)->toHtml() ?>
= $block->escapeHtml($_item->getDescription()) ?>
|
- = /* @noEscape */ $block->prepareSku($block->getSku()) ?> |
+
+ = /* @noEscape */ $block->prepareSku($block->getSku()) ?>
+ |
= $block->getItemPriceHtml() ?>
|
- getItem()->getQtyOrdered() > 0) : ?>
+ getItem()->getQtyOrdered() > 0): ?>
-
= $block->escapeHtml(__('Ordered')) ?>
= (float) $block->getItem()->getQtyOrdered() ?>
- getItem()->getQtyShipped() > 0) : ?>
+ getItem()->getQtyShipped() > 0): ?>
-
= $block->escapeHtml(__('Shipped')) ?>
= (float) $block->getItem()->getQtyShipped() ?>
- getItem()->getQtyCanceled() > 0) : ?>
+ getItem()->getQtyCanceled() > 0): ?>
-
= $block->escapeHtml(__('Canceled')) ?>
= (float) $block->getItem()->getQtyCanceled() ?>
- getItem()->getQtyRefunded() > 0) : ?>
+ getItem()->getQtyRefunded() > 0): ?>
-
= $block->escapeHtml(__('Refunded')) ?>
= (float) $block->getItem()->getQtyRefunded() ?>
diff --git a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml
index 26fe74b0fc454..6c7567a8cd14b 100644
--- a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml
+++ b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml
@@ -9,15 +9,15 @@
= $block->escapeHtml($_item->getName()) ?>
- getItemOptions()) : ?>
+ getItemOptions()): ?>
-
+
- = $block->escapeHtml($_option['label']) ?>
- getPrintStatus()) : ?>
+ getPrintStatus()): ?>
getFormatedOptionValue($_option) ?>
- >
- = $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?>
-
+ = $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?>
+
- = $block->escapeHtml($_option['label']) ?>
@@ -26,18 +26,21 @@
-
- - = $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?>
+
+
+ - = $block->escapeHtml($optionValue) ?>
getProductAdditionalInformationBlock(); ?>
-
+
= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?>
= $block->escapeHtml($_item->getDescription()) ?>
|
- = /* @noEscape */ $block->prepareSku($block->getSku()) ?> |
+
+ = /* @noEscape */ $block->prepareSku($block->getSku()) ?>
+ |
= (int) $_item->getQty() ?> |
diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php
index 53459f2c3e52f..d1440a2b547a4 100644
--- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php
+++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php
@@ -15,13 +15,14 @@
use Magento\Framework\View\Result\Layout;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\App\Action\HttpGetActionInterface;
+use Magento\Framework\App\Action\HttpPostActionInterface;
/**
* Export Coupons to csv file
*
* Class \Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCouponsCsv
*/
-class ExportCouponsCsv extends Quote implements HttpGetActionInterface
+class ExportCouponsCsv extends Quote implements HttpGetActionInterface, HttpPostActionInterface
{
/**
* Export coupon codes as CSV file
diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php
index fa3d4455410c4..401d8aea1aded 100644
--- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php
+++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php
@@ -15,13 +15,14 @@
use Magento\Framework\View\Result\Layout;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\App\Action\HttpGetActionInterface;
+use Magento\Framework\App\Action\HttpPostActionInterface;
/**
* Export coupons to xml file
*
* Class \Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCouponsXml
*/
-class ExportCouponsXml extends Quote implements HttpGetActionInterface
+class ExportCouponsXml extends Quote implements HttpGetActionInterface, HttpPostActionInterface
{
/**
* Export coupon codes as excel xml file
diff --git a/app/code/Magento/Tax/Pricing/Render/Adjustment.php b/app/code/Magento/Tax/Pricing/Render/Adjustment.php
index 8613e62f2983e..0e5c619790a97 100644
--- a/app/code/Magento/Tax/Pricing/Render/Adjustment.php
+++ b/app/code/Magento/Tax/Pricing/Render/Adjustment.php
@@ -38,6 +38,8 @@ public function __construct(
}
/**
+ * Apply the right HTML output to the adjustment
+ *
* @return string
*/
protected function apply()
@@ -173,4 +175,16 @@ public function displayPriceExcludingTax()
{
return $this->taxHelper->displayPriceExcludingTax();
}
+
+ /**
+ * Obtain a value for data-price-type attribute
+ *
+ * @return string
+ */
+ public function getDataPriceType(): string
+ {
+ return $this->amountRender->getPriceType() === 'finalPrice'
+ ? 'basePrice'
+ : 'base' . ucfirst($this->amountRender->getPriceType());
+ }
}
diff --git a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml
index e87d1c9eb96aa..685893151bc5a 100644
--- a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml
+++ b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml
@@ -6,12 +6,13 @@
?>
+
-displayBothPrices()) : ?>
- displayBothPrices()): ?>
+
= /* @noEscape */ $block->getDisplayAmountExclTax() ?>
diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml
new file mode 100644
index 0000000000000..c7236c33e7cc0
--- /dev/null
+++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js
index 9a34e57df86c7..65443fadf8007 100644
--- a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js
+++ b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js
@@ -668,7 +668,7 @@ define([
* @returns {Object} Chainable
*/
toggleListVisible: function () {
- this.listVisible(!this.listVisible());
+ this.listVisible(!this.disabled() && !this.listVisible());
return this;
},
diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js
index fe33389eabad4..848ad60219a2b 100644
--- a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js
+++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js
@@ -200,6 +200,7 @@ define([
* @returns {Filters} Chainable.
*/
apply: function () {
+ $('body').notification('clear');
this.set('applied', removeEmpty(this.filters));
return this;
diff --git a/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js
index be9044143c5a4..3c5e72d4d66ed 100644
--- a/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js
+++ b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js
@@ -13,10 +13,12 @@ define([
return Component.extend({
defaults: {
listingNamespace: null,
+ bookmarkProvider: 'componentType = bookmark, ns = ${ $.listingNamespace }',
filterProvider: 'componentType = filters, ns = ${ $.listingNamespace }',
filterKey: 'filters',
searchString: location.search,
modules: {
+ bookmarks: '${ $.bookmarkProvider }',
filterComponent: '${ $.filterProvider }'
}
},
@@ -49,6 +51,16 @@ define([
return;
}
+ if (!_.isUndefined(this.bookmarks())) {
+ if (!_.size(this.bookmarks().getViewData(this.bookmarks().defaultIndex))) {
+ setTimeout(function () {
+ this.apply();
+ }.bind(this), 500);
+
+ return;
+ }
+ }
+
if (Object.keys(urlFilter).length) {
applied = this.filterComponent().get('applied');
filters = $.extend({}, applied, urlFilter);
diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php
index 195c3f397ff18..b05b70cfcbc71 100644
--- a/app/code/Magento/Widget/Model/Widget.php
+++ b/app/code/Magento/Widget/Model/Widget.php
@@ -5,6 +5,16 @@
*/
namespace Magento\Widget\Model;
+use Magento\Framework\App\Cache\Type\Config;
+use Magento\Framework\DataObject;
+use Magento\Framework\Escaper;
+use Magento\Framework\Math\Random;
+use Magento\Framework\View\Asset\Repository;
+use Magento\Framework\View\Asset\Source;
+use Magento\Framework\View\FileSystem;
+use Magento\Widget\Helper\Conditions;
+use Magento\Widget\Model\Config\Data;
+
/**
* Widget model for different purposes
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -15,32 +25,32 @@
class Widget
{
/**
- * @var \Magento\Widget\Model\Config\Data
+ * @var Data
*/
protected $dataStorage;
/**
- * @var \Magento\Framework\App\Cache\Type\Config
+ * @var Config
*/
protected $configCacheType;
/**
- * @var \Magento\Framework\View\Asset\Repository
+ * @var Repository
*/
protected $assetRepo;
/**
- * @var \Magento\Framework\View\Asset\Source
+ * @var Source
*/
protected $assetSource;
/**
- * @var \Magento\Framework\View\FileSystem
+ * @var FileSystem
*/
protected $viewFileSystem;
/**
- * @var \Magento\Framework\Escaper
+ * @var Escaper
*/
protected $escaper;
@@ -50,30 +60,35 @@ class Widget
protected $widgetsArray = [];
/**
- * @var \Magento\Widget\Helper\Conditions
+ * @var Conditions
*/
protected $conditionsHelper;
/**
- * @var \Magento\Framework\Math\Random
+ * @var Random
*/
private $mathRandom;
/**
- * @param \Magento\Framework\Escaper $escaper
- * @param \Magento\Widget\Model\Config\Data $dataStorage
- * @param \Magento\Framework\View\Asset\Repository $assetRepo
- * @param \Magento\Framework\View\Asset\Source $assetSource
- * @param \Magento\Framework\View\FileSystem $viewFileSystem
- * @param \Magento\Widget\Helper\Conditions $conditionsHelper
+ * @var string[]
+ */
+ private $reservedChars = ['}', '{'];
+
+ /**
+ * @param Escaper $escaper
+ * @param Data $dataStorage
+ * @param Repository $assetRepo
+ * @param Source $assetSource
+ * @param FileSystem $viewFileSystem
+ * @param Conditions $conditionsHelper
*/
public function __construct(
- \Magento\Framework\Escaper $escaper,
- \Magento\Widget\Model\Config\Data $dataStorage,
- \Magento\Framework\View\Asset\Repository $assetRepo,
- \Magento\Framework\View\Asset\Source $assetSource,
- \Magento\Framework\View\FileSystem $viewFileSystem,
- \Magento\Widget\Helper\Conditions $conditionsHelper
+ Escaper $escaper,
+ Data $dataStorage,
+ Repository $assetRepo,
+ Source $assetSource,
+ FileSystem $viewFileSystem,
+ Conditions $conditionsHelper
) {
$this->escaper = $escaper;
$this->dataStorage = $dataStorage;
@@ -110,14 +125,11 @@ public function getWidgetByClassType($type)
$widgets = $this->getWidgets();
/** @var array $widget */
foreach ($widgets as $widget) {
- if (isset($widget['@'])) {
- if (isset($widget['@']['type'])) {
- if ($type === $widget['@']['type']) {
- return $widget;
- }
- }
+ if (isset($widget['@']['type']) && $type === $widget['@']['type']) {
+ return $widget;
}
}
+
return null;
}
@@ -131,6 +143,7 @@ public function getWidgetByClassType($type)
*/
public function getConfigAsXml($type)
{
+ // phpstan:ignore
return $this->getXmlElementByType($type);
}
@@ -296,42 +309,70 @@ public function getWidgetsArray($filters = [])
*/
public function getWidgetDeclaration($type, $params = [], $asIs = true)
{
- $directive = '{{widget type="' . $type . '"';
$widget = $this->getConfigAsObject($type);
+ $params = array_filter($params, function ($value) {
+ return $value !== null && $value !== '';
+ });
+
+ $directiveParams = '';
foreach ($params as $name => $value) {
// Retrieve default option value if pre-configured
- if ($name == 'conditions') {
- $name = 'conditions_encoded';
- $value = $this->conditionsHelper->encode($value);
- } elseif (is_array($value)) {
- $value = implode(',', $value);
- } elseif (trim($value) == '') {
- $parameters = $widget->getParameters();
- if (isset($parameters[$name]) && is_object($parameters[$name])) {
- $value = $parameters[$name]->getValue();
- }
- }
- if (isset($value)) {
- $directive .= sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false));
- }
+ $directiveParams .= $this->getDirectiveParam($widget, $name, $value);
}
- $directive .= $this->getWidgetPageVarName($params);
-
- $directive .= '}}';
+ $directive = sprintf('{{widget type="%s"%s%s}}', $type, $directiveParams, $this->getWidgetPageVarName($params));
if ($asIs) {
return $directive;
}
- $html = sprintf(
+ return sprintf(
' ',
$this->idEncode($directive),
$this->getPlaceholderImageUrl($type),
$this->escaper->escapeUrl($directive)
);
- return $html;
+ }
+
+ /**
+ * Returns directive param with prepared value
+ *
+ * @param DataObject $widget
+ * @param string $name
+ * @param string|array $value
+ * @return string
+ */
+ private function getDirectiveParam(DataObject $widget, string $name, $value): string
+ {
+ if ($name === 'conditions') {
+ $name = 'conditions_encoded';
+ $value = $this->conditionsHelper->encode($value);
+ } elseif (is_array($value)) {
+ $value = implode(',', $value);
+ } elseif (trim($value) === '') {
+ $parameters = $widget->getParameters();
+ if (isset($parameters[$name]) && is_object($parameters[$name])) {
+ $value = $parameters[$name]->getValue();
+ }
+ } else {
+ $value = $this->getPreparedValue($value);
+ }
+
+ return sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false));
+ }
+
+ /**
+ * Returns encoded value if it contains reserved chars
+ *
+ * @param string $value
+ * @return string
+ */
+ private function getPreparedValue(string $value): string
+ {
+ $pattern = sprintf('/%s/', implode('|', $this->reservedChars));
+
+ return preg_match($pattern, $value) ? rawurlencode($value) : $value;
}
/**
diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php
index 5d9b1911bc292..7d30d958b5228 100644
--- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php
+++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php
@@ -398,7 +398,11 @@ protected function _renderFiltersBefore()
$availableProductTypes = $this->salesConfig->getAvailableProductTypes();
$this->getSelect()->join(
['cat_prod' => $this->getTable('catalog_product_entity')],
- $this->getConnection()->quoteInto('cat_prod.type_id IN (?)', $availableProductTypes),
+ $this->getConnection()
+ ->quoteInto(
+ "cat_prod.type_id IN (?) AND {$mainTableName}.product_id = cat_prod.entity_id",
+ $availableProductTypes
+ ),
[]
);
}
diff --git a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less
index fa158589feb96..654236e143a29 100644
--- a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less
+++ b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less
@@ -18,15 +18,10 @@
// _____________________________________________
.currency-addon {
+ .lib-vendor-prefix-display(inline-flex);
border: 1px solid rgb(173,173,173);
- position: relative;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
- -webkit-flex-direction: row;
- -ms-flex-direction: row;
- flex-direction: row;
flex-flow: row nowrap;
+ position: relative;
width: 100%;
.admin__control-text {
diff --git a/app/design/frontend/Magento/blank/web/css/source/_navigation.less b/app/design/frontend/Magento/blank/web/css/source/_navigation.less
index fad906a089400..f9cca1ca16a18 100644
--- a/app/design/frontend/Magento/blank/web/css/source/_navigation.less
+++ b/app/design/frontend/Magento/blank/web/css/source/_navigation.less
@@ -28,10 +28,10 @@
.nav-toggle {
.lib-icon-font(
- @icon-menu,
- @_icon-font-size: 28px,
- @_icon-font-color: @header-icons-color,
- @_icon-font-color-hover: @header-icons-color-hover
+ @icon-menu,
+ @_icon-font-size: 28px,
+ @_icon-font-color: @header-icons-color,
+ @_icon-font-color-hover: @header-icons-color-hover
);
.lib-icon-text-hide();
cursor: pointer;
@@ -54,13 +54,13 @@
.parent {
.level-top {
- position: relative;
.lib-icon-font(
- @_icon-font-content: @icon-down,
- @_icon-font-size: 42px,
- @_icon-font-position: after,
- @_icon-font-display: block
+ @_icon-font-content: @icon-down,
+ @_icon-font-size: 42px,
+ @_icon-font-position: after,
+ @_icon-font-display: block
);
+ position: relative;
&:after {
position: absolute;
@@ -70,8 +70,8 @@
&.ui-state-active {
.lib-icon-font-symbol(
- @_icon-font-content: @icon-up,
- @_icon-font-position: after
+ @_icon-font-content: @icon-up,
+ @_icon-font-position: after
);
}
}
@@ -82,12 +82,10 @@
-webkit-overflow-scrolling: touch;
.lib-css(transition, left .3s, 1);
height: 100%;
- left: -80%;
left: calc(~'-1 * (100% - @{active-nav-indent})');
overflow: auto;
position: fixed;
top: 0;
- width: 80%;
width: calc(~'100% - @{active-nav-indent}');
.switcher {
@@ -109,13 +107,13 @@
.switcher-trigger {
strong {
- position: relative;
.lib-icon-font(
- @_icon-font-content: @icon-down,
- @_icon-font-size: 42px,
- @_icon-font-position: after,
- @_icon-font-display: block
+ @_icon-font-content: @icon-down,
+ @_icon-font-size: 42px,
+ @_icon-font-position: after,
+ @_icon-font-display: block
);
+ position: relative;
&:after {
position: absolute;
@@ -126,16 +124,18 @@
&.active strong {
.lib-icon-font-symbol(
- @_icon-font-content: @icon-up,
- @_icon-font-position: after
+ @_icon-font-content: @icon-up,
+ @_icon-font-position: after
);
}
}
+
.switcher-dropdown {
.lib-list-reset-styles();
display: none;
padding: @indent__s 0;
}
+
.switcher-options {
&.active {
.switcher-dropdown {
@@ -143,6 +143,7 @@
}
}
}
+
.header.links {
.lib-list-reset-styles();
border-bottom: 1px solid @color-gray82;
@@ -200,13 +201,11 @@
.nav-open {
.page-wrapper {
- left: 80%;
left: calc(~'100% - @{active-nav-indent}');
}
.nav-sections {
@_shadow: 0 0 5px 0 rgba(50, 50, 50, .75);
-
.lib-css(box-shadow, @_shadow, 1);
left: 0;
z-index: 99;
@@ -293,10 +292,6 @@
display: none;
}
- .nav-sections-item-content {
- display: block !important;
- }
-
.nav-sections-item-content > * {
display: none;
}
diff --git a/composer.json b/composer.json
index 1af86e438882c..57fbfaaa35c2b 100644
--- a/composer.json
+++ b/composer.json
@@ -222,6 +222,8 @@
"magento/module-media-gallery-cms-ui": "*",
"magento/module-media-gallery-catalog-integration": "*",
"magento/module-media-gallery-catalog": "*",
+ "magento/module-media-gallery-renditions": "*",
+ "magento/module-media-gallery-renditions-api": "*",
"magento/module-media-storage": "*",
"magento/module-message-queue": "*",
"magento/module-msrp": "*",
diff --git a/composer.lock b/composer.lock
index 551167152be4d..8a5d82536cee4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "aadcf8a265dd7ecbb86dd3dd4e49bc28",
+ "content-hash": "a03edc1c8ee05f82886eebd6ed288df8",
"packages": [
{
"name": "colinmollenhour/cache-backend-file",
diff --git a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php
index 7a4f472c69513..538c0b0ee5fac 100644
--- a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php
@@ -225,6 +225,7 @@ public function testUpdateBundleAddSelection()
public function testUpdateBundleAddAndDeleteOption()
{
$bundleProduct = $this->createDynamicBundleProduct();
+ $linkedProductPrice = 20;
$bundleProductOptions = $this->getBundleProductOptions($bundleProduct);
@@ -238,7 +239,7 @@ public function testUpdateBundleAddAndDeleteOption()
[
'sku' => 'simple2',
'qty' => 2,
- "price" => 20,
+ "price" => $linkedProductPrice,
"price_type" => 1,
"is_default" => false,
],
@@ -256,6 +257,7 @@ public function testUpdateBundleAddAndDeleteOption()
$this->assertFalse(isset($bundleOptions[1]));
$this->assertEquals('simple2', $bundleOptions[0]['product_links'][0]['sku']);
$this->assertEquals(2, $bundleOptions[0]['product_links'][0]['qty']);
+ $this->assertEquals($linkedProductPrice, $bundleOptions[0]['product_links'][0]['price']);
}
/**
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php
index c2f94128ef8ec..cb210b180682c 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php
@@ -10,7 +10,7 @@
use Magento\TestFramework\TestCase\GraphQlAbstract;
/**
- * Get related products test
+ * Test coverage for get related products
*/
class GetRelatedProductsTest extends GraphQlAbstract
{
@@ -49,6 +49,40 @@ public function testQueryRelatedProducts()
self::assertRelatedProducts($relatedProducts);
}
+ /**
+ * @magentoApiDataFixture Magento/Catalog/_files/products_related_disabled.php
+ */
+ public function testQueryDisableRelatedProduct()
+ {
+ $productSku = 'simple_with_cross';
+
+ $query = <<graphQlQuery($query);
+
+ self::assertArrayHasKey('products', $response);
+ self::assertArrayHasKey('items', $response['products']);
+ self::assertCount(1, $response['products']['items']);
+ self::assertArrayHasKey(0, $response['products']['items']);
+ self::assertArrayHasKey('related_products', $response['products']['items'][0]);
+ $relatedProducts = $response['products']['items'][0]['related_products'];
+ self::assertCount(0, $relatedProducts);
+ }
+
/**
* @magentoApiDataFixture Magento/Catalog/_files/products_crosssell.php
*/
diff --git a/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php b/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php
new file mode 100644
index 0000000000000..85007ad560d53
--- /dev/null
+++ b/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php
@@ -0,0 +1,24 @@
+helperProduct = $this->objectManager->get(HelperProduct::class);
+ $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php
+ * @return void
+ */
+ public function testRenderCustomOptionsWithoutOptions(): void
+ {
+ $product = $this->productRepository->get('simple');
+ $this->assertEquals(
+ 0,
+ Xpath::getElementsCountForXpath(
+ "//fieldset[@id='product_composite_configure_fields_options']",
+ $this->getOptionHtml($product)
+ ),
+ 'The option block is expected to be empty!'
+ );
+ }
+
+ /**
+ * Check that options from text group(field, area) render as expected.
+ *
+ * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php
+ * @dataProvider renderCustomOptionsFromTextGroupProvider
+ * @param array $optionData
+ * @param array $checkArray
+ * @return void
+ */
+ public function testRenderCustomOptionsFromTextGroup(array $optionData, array $checkArray): void
+ {
+ $this->assertTextOptionRenderingOnProduct('simple', $optionData, $checkArray);
+ }
+
+ /**
+ * Provides test data to verify the display of text type options.
+ *
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @return array
+ */
+ public function renderCustomOptionsFromTextGroupProvider(): array
+ {
+ return [
+ 'type_text_required_field' => [
+ [
+ Option::KEY_TITLE => 'Test option type text 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD,
+ Option::KEY_IS_REQUIRE => 0,
+ Option::KEY_PRICE => 0,
+ Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Option::KEY_MAX_CHARACTERS => 0,
+ ],
+ [
+ 'contains' => [
+ 'block_with_required_class' => '',
+ 'title' => 'Test option type text 1',
+ ],
+ 'equals_xpath' => [
+ 'zero_price' => [
+ 'xpath' => "//label[contains(@class, 'admin__field-label')]/span",
+ 'message' => 'Expected empty price is incorrect or missing!',
+ 'expected' => 0,
+ ],
+ ],
+ ],
+ ],
+ 'type_text_is_required_option' => [
+ [
+ Option::KEY_TITLE => 'Test option type text 2',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD,
+ Option::KEY_IS_REQUIRE => 1,
+ Option::KEY_PRICE => 0,
+ Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Option::KEY_MAX_CHARACTERS => 0,
+ ],
+ [
+ 'contains' => [
+ 'block_with_required_class' => ' ',
+ ],
+ ],
+ ],
+ 'type_text_fixed_positive_price' => [
+ [
+ Option::KEY_TITLE => 'Test option type text 3',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD,
+ Option::KEY_IS_REQUIRE => 0,
+ Option::KEY_PRICE => 50,
+ Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Option::KEY_MAX_CHARACTERS => 0,
+ ],
+ [
+ 'contains' => [
+ 'price' => 'data-price-amount="50"',
+ ],
+ 'equals_xpath' => [
+ 'sign_price' => [
+ 'xpath' => "//label[contains(@class, 'admin__field-label')]/span[contains(text(), '+')]",
+ 'message' => 'Expected positive price is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_text_fixed_negative_price' => [
+ [
+ Option::KEY_TITLE => 'Test option type text 4',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD,
+ Option::KEY_IS_REQUIRE => 0,
+ Option::KEY_PRICE => -50,
+ Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Option::KEY_MAX_CHARACTERS => 0,
+ ],
+ [
+ 'contains' => [
+ 'price' => 'data-price-amount="50"',
+ ],
+ 'equals_xpath' => [
+ 'sign_price' => [
+ 'xpath' => "//label[contains(@class, 'admin__field-label')]/span[contains(text(), '-')]",
+ 'message' => 'Expected negative price is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_text_percent_price' => [
+ [
+ Option::KEY_TITLE => 'Test option type text 5',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD,
+ Option::KEY_IS_REQUIRE => 0,
+ Option::KEY_PRICE => 50,
+ Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_PERCENT,
+ Option::KEY_MAX_CHARACTERS => 0,
+ ],
+ [
+ 'contains' => [
+ 'price' => 'data-price-amount="5"',
+ ],
+ ],
+ ],
+ 'type_text_max_characters' => [
+ [
+ Option::KEY_TITLE => 'Test option type text 6',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD,
+ Option::KEY_IS_REQUIRE => 0,
+ Option::KEY_PRICE => 10,
+ Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Option::KEY_MAX_CHARACTERS => 99,
+ ],
+ [
+ 'max_characters' => (string)__('Maximum number of characters:') . ' 99',
+ ],
+ ],
+ 'type_field' => [
+ [
+ Option::KEY_TITLE => 'Test option type field 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD,
+ Option::KEY_IS_REQUIRE => 0,
+ Option::KEY_PRICE => 10,
+ Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Option::KEY_MAX_CHARACTERS => 0,
+ 'configure_option_value' => 'Type field option value',
+ ],
+ [
+ 'equals_xpath' => [
+ 'control_price_attribute' => [
+ 'xpath' => "//input[@id='options_%s_text' and @price='%s']",
+ 'message' => 'Expected input price is incorrect or missing!',
+ ],
+ 'default_option_value' => [
+ 'xpath' => "//input[@id='options_%s_text' and @value='Type field option value']",
+ 'message' => 'Expected input default value is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_area' => [
+ [
+ Option::KEY_TITLE => 'Test option type area 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_AREA,
+ Option::KEY_IS_REQUIRE => 0,
+ Option::KEY_PRICE => 10,
+ Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Option::KEY_MAX_CHARACTERS => 0,
+ 'configure_option_value' => 'Type area option value',
+ ],
+ [
+ 'equals_xpath' => [
+ 'control_price_attribute' => [
+ 'xpath' => "//textarea[@id='options_%s_text' and @price='%s']",
+ 'message' => 'Expected textarea price is incorrect or missing!',
+ ],
+ 'default_option_value' => [
+ 'xpath' => "//textarea[@id='options_%s_text' "
+ . "and contains(text(), 'Type area option value')]",
+ 'message' => 'Expected textarea default value is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Check that options from select group(drop-down, radio buttons, checkbox, multiple select) render as expected.
+ *
+ * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php
+ * @dataProvider renderCustomOptionsFromSelectGroupProvider
+ * @param array $optionData
+ * @param array $optionValueData
+ * @param array $checkArray
+ * @return void
+ */
+ public function testRenderCustomOptionsFromSelectGroup(
+ array $optionData,
+ array $optionValueData,
+ array $checkArray
+ ): void {
+ $this->assertSelectOptionRenderingOnProduct('simple', $optionData, $optionValueData, $checkArray);
+ }
+
+ /**
+ * Provides test data to verify the display of select type options.
+ *
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @return array
+ */
+ public function renderCustomOptionsFromSelectGroupProvider(): array
+ {
+ return [
+ 'type_select_required_field' => [
+ [
+ Option::KEY_TITLE => 'Test option type select 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN,
+ Option::KEY_IS_REQUIRE => 0,
+ ],
+ [
+ Value::KEY_TITLE => 'Select value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ ],
+ [
+ 'contains' => [
+ 'block_with_required_class' => ' ',
+ 'title' => ' Test option type select 1',
+ ],
+ 'equals_xpath' => [
+ 'required_element' => [
+ 'xpath' => "//select[@id='select_%s']",
+ 'message' => 'Expected select type is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_select_is_required_option' => [
+ [
+ Option::KEY_TITLE => 'Test option type select 2',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN,
+ Option::KEY_IS_REQUIRE => 1,
+ ],
+ [
+ Value::KEY_TITLE => 'Select value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ ],
+ [
+ 'contains' => [
+ 'block_with_required_class' => ' ',
+ ],
+ ],
+ ],
+ 'type_drop_down_with_selected' => [
+ [
+ Option::KEY_TITLE => 'Test option type drop-down 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN,
+ Option::KEY_IS_REQUIRE => 0,
+ 'configure_option_value' => 'Drop-down value 1',
+ ],
+ [
+ Value::KEY_TITLE => 'Drop-down value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ ],
+ [
+ 'equals_xpath' => [
+ 'element_type' => [
+ 'xpath' => "//select[contains(@class, 'admin__control-select')]",
+ 'message' => 'Expected drop down type is incorrect or missing!',
+ ],
+ 'default_value' => [
+ 'xpath' => "//option[contains(text(), '" . __('-- Please Select --') . "')]",
+ 'message' => 'Expected default value is incorrect or missing!',
+ ],
+ 'selected_value' => [
+ 'xpath' => "//option[@selected='selected' and contains(text(), 'Drop-down value 1')]",
+ 'message' => 'Expected selected value is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_multiple_with_selected' => [
+ [
+ Option::KEY_TITLE => 'Test option type multiple 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE,
+ Option::KEY_IS_REQUIRE => 0,
+ 'configure_option_value' => 'Multiple value 1',
+ ],
+ [
+ Value::KEY_TITLE => 'Multiple value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ ],
+ [
+ 'equals_xpath' => [
+ 'element_type' => [
+ 'xpath' => "//select[contains(@class, 'admin__control-multiselect') "
+ . "and @multiple='multiple']",
+ 'message' => 'Expected multiple type is incorrect or missing!',
+ ],
+ 'selected_value' => [
+ 'xpath' => "//option[@selected='selected' and contains(text(), 'Multiple value 1')]",
+ 'message' => 'Expected selected value is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_checkable_required_field' => [
+ [
+ Option::KEY_TITLE => 'Test option type checkable 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO,
+ Option::KEY_IS_REQUIRE => 0,
+ ],
+ [
+ Value::KEY_TITLE => 'Checkable value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ ],
+ [
+ 'equals_xpath' => [
+ 'required_checkable_option' => [
+ 'xpath' => "//div[@id='options-%s-list']",
+ 'message' => 'Expected checkable option is incorrect or missing!',
+ ],
+ 'option_value_title' => [
+ 'xpath' => "//label[@for='options_%s_2']/span[contains(text(), 'Checkable value 1')]",
+ 'message' => 'Expected option value title is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_radio_is_required_option' => [
+ [
+ Option::KEY_TITLE => 'Test option type radio 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO,
+ Option::KEY_IS_REQUIRE => 1,
+ ],
+ [
+ Value::KEY_TITLE => 'Radio value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ ],
+ [
+ 'equals_xpath' => [
+ 'span_container' => [
+ 'xpath' => "//span[@id='options-%s-container']",
+ 'message' => 'Expected span container is incorrect or missing!',
+ ],
+ 'default_option_value' => [
+ 'xpath' => "//label[@for='options_%s']/span[contains(text(), '" . __('None') . "')]",
+ 'message' => 'Expected default option value is incorrect or missing!',
+ 'expected' => 0,
+ ],
+ ],
+ ],
+ ],
+ 'type_radio_with_selected' => [
+ [
+ Option::KEY_TITLE => 'Test option type radio 2',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO,
+ Option::KEY_IS_REQUIRE => 0,
+ 'configure_option_value' => 'Radio value 1',
+ ],
+ [
+ Value::KEY_TITLE => 'Radio value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ ],
+ [
+ 'equals_xpath' => [
+ 'default_option_value' => [
+ 'xpath' => "//label[@for='options_%s']/span[contains(text(), '" . __('None') . "')]",
+ 'message' => 'Expected default option value is incorrect or missing!',
+ ],
+ 'element_type' => [
+ 'xpath' => "//input[@id='options_%s_2' and contains(@class, 'admin__control-radio')]",
+ 'message' => 'Expected radio type is incorrect or missing!',
+ ],
+ 'selected_value' => [
+ 'xpath' => "//input[@id='options_%s_2' and @checked='checked']",
+ 'message' => 'Expected selected option value is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_checkbox_is_required_option' => [
+ [
+ Option::KEY_TITLE => 'Test option type checkbox 1',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX,
+ Option::KEY_IS_REQUIRE => 1,
+ ],
+ [
+ Value::KEY_TITLE => 'Checkbox value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Value::KEY_SKU => '',
+ ],
+ [
+ 'equals_xpath' => [
+ 'span_container' => [
+ 'xpath' => "//span[@id='options-%s-container']",
+ 'message' => 'Expected span container is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ 'type_checkbox_with_selected' => [
+ [
+ Option::KEY_TITLE => 'Test option type checkbox 2',
+ Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX,
+ Option::KEY_IS_REQUIRE => 0,
+ 'configure_option_value' => 'Checkbox value 1',
+ ],
+ [
+ Value::KEY_TITLE => 'Checkbox value 1',
+ Value::KEY_PRICE => 10,
+ Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED,
+ Value::KEY_SKU => '',
+ ],
+ [
+ 'equals_xpath' => [
+ 'element_type' => [
+ 'xpath' => "//input[@id='options_%s_2' and contains(@class, 'admin__control-checkbox')]",
+ 'message' => 'Expected checkbox type is incorrect or missing!',
+ ],
+ 'selected_value' => [
+ 'xpath' => "//input[@id='options_%s_2' and @checked='checked']",
+ 'message' => 'Expected selected option value is incorrect or missing!',
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function addOptionToProduct(
+ ProductInterface $product,
+ array $optionData,
+ array $optionValueData = []
+ ): ProductInterface {
+ $product = parent::addOptionToProduct($product, $optionData, $optionValueData);
+
+ if (isset($optionData['configure_option_value'])) {
+ $optionValue = $optionData['configure_option_value'];
+ $option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]);
+ if (!empty($optionValueData)) {
+ $optionValueObject = $this->findOptionValueByTitle($option, $optionValue);
+ $optionValue = $option->getType() === Option::OPTION_TYPE_CHECKBOX
+ ? [$optionValueObject->getOptionTypeId()]
+ : $optionValueObject->getOptionTypeId();
+ }
+ /** @var DataObject $request */
+ $buyRequest = $this->dataObjectFactory->create();
+ $buyRequest->setData([
+ 'qty' => 1,
+ 'options' => [$option->getId() => $optionValue],
+ ]);
+ $this->helperProduct->prepareProductOptions($product, $buyRequest);
+ }
+
+ return $product;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function baseOptionAsserts(
+ ProductCustomOptionInterface $option,
+ string $optionHtml,
+ array $checkArray
+ ): void {
+ if (isset($checkArray['contains'])) {
+ foreach ($checkArray['contains'] as $needle) {
+ $this->assertStringContainsString($needle, $optionHtml);
+ }
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function additionalTypeTextAsserts(
+ ProductCustomOptionInterface $option,
+ string $optionHtml,
+ array $checkArray
+ ): void {
+ parent::additionalTypeTextAsserts($option, $optionHtml, $checkArray);
+
+ if (isset($checkArray['equals_xpath'])) {
+ foreach ($checkArray['equals_xpath'] as $key => $value) {
+ $value['args'] = $key === 'control_price_attribute' ? [(float)$option->getPrice()] : [];
+ $this->assertEqualsXpath($option, $optionHtml, $value);
+ }
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function additionalTypeSelectAsserts(
+ ProductCustomOptionInterface $option,
+ string $optionHtml,
+ array $checkArray
+ ): void {
+ parent::additionalTypeSelectAsserts($option, $optionHtml, $checkArray);
+
+ if (isset($checkArray['equals_xpath'])) {
+ foreach ($checkArray['equals_xpath'] as $value) {
+ $this->assertEqualsXpath($option, $optionHtml, $value);
+ }
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getHandlesList(): array
+ {
+ return [
+ 'default',
+ 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE',
+ 'catalog_product_view_type_simple',
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getMaxCharactersCssClass(): string
+ {
+ return 'class="note"';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getOptionsBlockName(): string
+ {
+ return 'product.composite.fieldset.options';
+ }
+
+ /**
+ * Checks that the xpath string is equal to the expected value
+ *
+ * @param ProductCustomOptionInterface $option
+ * @param string $html
+ * @param array $xpathData
+ * @return void
+ */
+ private function assertEqualsXpath(ProductCustomOptionInterface $option, string $html, array $xpathData): void
+ {
+ $args = array_merge([$option->getOptionId()], $xpathData['args'] ?? []);
+ $expected = $xpathData['expected'] ?? 1;
+ $this->assertEquals(
+ $expected,
+ Xpath::getElementsCountForXpath(sprintf($xpathData['xpath'], ...$args), $html),
+ $xpathData['message']
+ );
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php
new file mode 100644
index 0000000000000..a51b51a73645f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php
@@ -0,0 +1,138 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Qty::class);
+ $this->registry = $this->objectManager->get(Registry::class);
+ $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
+ $this->productRepository->cleanCache();
+ $this->helperProduct = $this->objectManager->get(HelperProduct::class);
+ $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->registry->unregister('current_product');
+ $this->registry->unregister('product');
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php
+ * @return void
+ */
+ public function testGetProduct(): void
+ {
+ $product = $this->productRepository->get('simple-1');
+ $this->registerProduct($product);
+ $this->assertEquals(
+ $product->getId(),
+ $this->block->getProduct()->getId(),
+ 'The expected product is missing in the Qty block!'
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php
+ * @dataProvider getQtyValueProvider
+ * @param bool $isQty
+ * @param int $qty
+ * @return void
+ */
+ public function testGetQtyValue(bool $isQty = false, int $qty = 1): void
+ {
+ $product = $this->productRepository->get('simple-1');
+ if ($isQty) {
+ /** @var DataObject $request */
+ $buyRequest = $this->dataObjectFactory->create();
+ $buyRequest->setData(['qty' => $qty]);
+ $this->helperProduct->prepareProductOptions($product, $buyRequest);
+ }
+ $this->registerProduct($product);
+ $this->assertEquals($qty, $this->block->getQtyValue(), 'Expected block qty value is incorrect!');
+ }
+
+ /**
+ * Provides test data to verify block qty value.
+ *
+ * @return array
+ */
+ public function getQtyValueProvider(): array
+ {
+ return [
+ 'with_qty' => [
+ 'is_qty' => true,
+ 'qty' => 5,
+ ],
+ 'without_qty' => [],
+ ];
+ }
+
+ /**
+ * Register the product
+ *
+ * @param ProductInterface $product
+ * @return void
+ */
+ private function registerProduct(ProductInterface $product): void
+ {
+ $this->registry->unregister('current_product');
+ $this->registry->unregister('product');
+ $this->registry->register('current_product', $product);
+ $this->registry->register('product', $product);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php
new file mode 100644
index 0000000000000..ab09314e18cc8
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php
@@ -0,0 +1,121 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->page = $this->objectManager->get(PageFactory::class)->create();
+ $this->registry = $this->objectManager->get(Registry::class);
+ $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
+ $this->productRepository->cleanCache();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->registry->unregister('current_product');
+ $this->registry->unregister('product');
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Catalog/_files/product_simple_with_options.php
+ * @return void
+ */
+ public function testRenderHtml(): void
+ {
+ $product = $this->productRepository->get('simple');
+ $this->registerProduct($product);
+ $this->preparePage();
+ $fieldsetBlock = $this->page->getLayout()->getBlock('product.composite.fieldset');
+ $this->assertNotFalse($fieldsetBlock, 'Expected fieldset block is missing!');
+ $html = $fieldsetBlock->toHtml();
+
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(sprintf($this->fieldsetXpath, 'options'), $html),
+ 'Expected options block is missing!'
+ );
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(sprintf($this->fieldsetXpath, 'qty'), $html),
+ 'Expected qty block is missing!'
+ );
+ }
+
+ /**
+ * Prepare page layout
+ *
+ * @return void
+ */
+ private function preparePage(): void
+ {
+ $this->page->addHandle([
+ 'default',
+ 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE',
+ 'catalog_product_view_type_simple',
+ ]);
+ $this->page->getLayout()->generateXml();
+ }
+
+ /**
+ * Register the product
+ *
+ * @param ProductInterface $product
+ * @return void
+ */
+ private function registerProduct(ProductInterface $product): void
+ {
+ $this->registry->unregister('current_product');
+ $this->registry->unregister('product');
+ $this->registry->register('current_product', $product);
+ $this->registry->register('product', $product);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php
index eb34696c70dbf..b575fc5e7033c 100644
--- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php
+++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php
@@ -9,14 +9,14 @@
use Magento\Catalog\Api\Data\ProductCustomOptionInterface;
use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory;
+use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface;
use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Block\Product\View\Options;
use Magento\Catalog\Model\Product\Option;
-use Magento\Catalog\Model\Product\Option\Value;
-use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Result\Page;
+use Magento\Framework\View\Result\PageFactory;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\ObjectManager;
use PHPUnit\Framework\TestCase;
@@ -29,12 +29,12 @@ abstract class AbstractRenderCustomOptionsTest extends TestCase
/**
* @var ObjectManager
*/
- private $objectManager;
+ protected $objectManager;
/**
* @var ProductRepositoryInterface
*/
- private $productRepository;
+ protected $productRepository;
/**
* @var ProductCustomOptionInterfaceFactory
@@ -57,12 +57,13 @@ abstract class AbstractRenderCustomOptionsTest extends TestCase
protected function setUp(): void
{
$this->objectManager = Bootstrap::getObjectManager();
- $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
+ $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
+ $this->productRepository->cleanCache();
$this->productCustomOptionFactory = $this->objectManager->get(ProductCustomOptionInterfaceFactory::class);
$this->productCustomOptionValuesFactory = $this->objectManager->get(
ProductCustomOptionValuesInterfaceFactory::class
);
- $this->page = $this->objectManager->create(Page::class);
+ $this->page = $this->objectManager->get(PageFactory::class)->create();
parent::setUp();
}
@@ -94,11 +95,26 @@ protected function assertTextOptionRenderingOnProduct(
$option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]);
$optionHtml = $this->getOptionHtml($product);
$this->baseOptionAsserts($option, $optionHtml, $checkArray);
+ $this->additionalTypeTextAsserts($option, $optionHtml, $checkArray);
+ }
- if ($optionData[Option::KEY_MAX_CHARACTERS] > 0) {
+ /**
+ * Additional asserts for rendering text type options.
+ *
+ * @param ProductCustomOptionInterface $option
+ * @param string $optionHtml
+ * @param array $checkArray
+ * @return void
+ */
+ protected function additionalTypeTextAsserts(
+ ProductCustomOptionInterface $option,
+ string $optionHtml,
+ array $checkArray
+ ): void {
+ if ($option->getMaxCharacters() > 0) {
$this->assertStringContainsString($checkArray['max_characters'], $optionHtml);
} else {
- $this->assertStringNotContainsString('class="character-counter', $optionHtml);
+ $this->assertStringNotContainsString($this->getMaxCharactersCssClass(), $optionHtml);
}
}
@@ -153,22 +169,36 @@ protected function assertSelectOptionRenderingOnProduct(
$product = $this->productRepository->get($productSku);
$product = $this->addOptionToProduct($product, $optionData, $optionValueData);
$option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]);
- $optionValues = $option->getValues();
- $optionValue = reset($optionValues);
$optionHtml = $this->getOptionHtml($product);
$this->baseOptionAsserts($option, $optionHtml, $checkArray);
+ $this->additionalTypeSelectAsserts($option, $optionHtml, $checkArray);
+ }
+ /**
+ * Additional asserts for rendering select type options.
+ *
+ * @param ProductCustomOptionInterface $option
+ * @param string $optionHtml
+ * @param array $checkArray
+ * @return void
+ */
+ protected function additionalTypeSelectAsserts(
+ ProductCustomOptionInterface $option,
+ string $optionHtml,
+ array $checkArray
+ ): void {
+ $optionValues = $option->getValues();
+ $optionValue = reset($optionValues);
if (isset($checkArray['not_contain_arr'])) {
foreach ($checkArray['not_contain_arr'] as $notContainPattern) {
$this->assertDoesNotMatchRegularExpression($notContainPattern, $optionHtml);
}
}
-
if (isset($checkArray['option_value_item'])) {
$checkArray['option_value_item'] = sprintf(
$checkArray['option_value_item'],
$optionValue->getOptionTypeId(),
- $optionValueData[Value::KEY_TITLE]
+ $optionValue->getTitle()
);
$this->assertMatchesRegularExpression($checkArray['option_value_item'], $optionHtml);
}
@@ -284,7 +314,7 @@ protected function assertDateOptionRenderingOnProduct(
* @param array $checkArray
* @return void
*/
- private function baseOptionAsserts(
+ protected function baseOptionAsserts(
ProductCustomOptionInterface $option,
string $optionHtml,
array $checkArray
@@ -317,7 +347,7 @@ private function baseOptionAsserts(
* @param array $optionValueData
* @return ProductInterface
*/
- private function addOptionToProduct(
+ protected function addOptionToProduct(
ProductInterface $product,
array $optionData,
array $optionValueData = []
@@ -341,28 +371,16 @@ private function addOptionToProduct(
* @param ProductInterface $product
* @return string
*/
- private function getOptionHtml(ProductInterface $product): string
- {
- $optionsBlock = $this->getOptionsBlock();
- $optionsBlock->setProduct($product);
-
- return $optionsBlock->toHtml();
- }
-
- /**
- * Get options block.
- *
- * @return Options
- */
- private function getOptionsBlock(): Options
+ protected function getOptionHtml(ProductInterface $product): string
{
$this->page->addHandle($this->getHandlesList());
$this->page->getLayout()->generateXml();
- /** @var Template $productInfoFormOptionsBlock */
- $productInfoFormOptionsBlock = $this->page->getLayout()->getBlock('product.info.form.options');
- $optionsWrapperBlock = $productInfoFormOptionsBlock->getChildBlock('product_options_wrapper');
+ /** @var Options $optionsBlock */
+ $optionsBlock = $this->page->getLayout()->getBlock($this->getOptionsBlockName());
+ $this->assertNotFalse($optionsBlock);
+ $optionsBlock->setProduct($product);
- return $optionsWrapperBlock->getChildBlock('product_options');
+ return $optionsBlock->toHtml();
}
/**
@@ -372,7 +390,7 @@ private function getOptionsBlock(): Options
* @param string $optionTitle
* @return null|Option
*/
- private function findOptionByTitle(ProductInterface $product, string $optionTitle): ?Option
+ protected function findOptionByTitle(ProductInterface $product, string $optionTitle): ?Option
{
$option = null;
foreach ($product->getOptions() as $customOption) {
@@ -385,10 +403,42 @@ private function findOptionByTitle(ProductInterface $product, string $optionTitl
return $option;
}
+ /**
+ * Find and return custom option value.
+ *
+ * @param ProductCustomOptionInterface $option
+ * @param string $optionValueTitle
+ * @return null|ProductCustomOptionValuesInterface
+ */
+ protected function findOptionValueByTitle(
+ ProductCustomOptionInterface $option,
+ string $optionValueTitle
+ ): ?ProductCustomOptionValuesInterface {
+ $optionValue = null;
+ foreach ($option->getValues() as $customOptionValue) {
+ if ($customOptionValue->getTitle() === $optionValueTitle) {
+ $optionValue = $customOptionValue;
+ break;
+ }
+ }
+
+ return $optionValue;
+ }
+
/**
* Return all need handles for load.
*
* @return array
*/
abstract protected function getHandlesList(): array;
+
+ /**
+ * @return string
+ */
+ abstract protected function getMaxCharactersCssClass(): string;
+
+ /**
+ * @return string
+ */
+ abstract protected function getOptionsBlockName(): string;
}
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php
index da31cfc74476a..83c249ed062e6 100644
--- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php
+++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php
@@ -10,7 +10,6 @@
/**
* Test cases related to check that simple product custom option renders as expected.
*
- * @magentoDbIsolation disabled
* @magentoAppArea frontend
*/
class RenderOptionsTest extends AbstractRenderCustomOptionsTest
@@ -89,4 +88,20 @@ protected function getHandlesList(): array
'catalog_product_view',
];
}
+
+ /**
+ * @inheritdoc
+ */
+ protected function getMaxCharactersCssClass(): string
+ {
+ return 'class="character-counter';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getOptionsBlockName(): string
+ {
+ return 'product.info.options';
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php
new file mode 100644
index 0000000000000..68d5c43434daa
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php
@@ -0,0 +1,40 @@
+create(\Magento\Catalog\Model\Product::class);
+$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
+ ->setAttributeSetId(4)
+ ->setName('Simple Related Product')
+ ->setSku('simple')
+ ->setPrice(10)
+ ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED)
+ ->setWebsiteIds([1])
+ ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1])
+ ->save();
+
+/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */
+$productLink = $objectManager->create(\Magento\Catalog\Api\Data\ProductLinkInterface::class);
+$productLink->setSku('simple_with_cross');
+$productLink->setLinkedProductSku('simple');
+$productLink->setPosition(1);
+$productLink->setLinkType('related');
+
+$product = $objectManager->create(\Magento\Catalog\Model\Product::class);
+$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE)
+ ->setAttributeSetId(4)
+ ->setName('Simple Product With Related Product')
+ ->setSku('simple_with_cross')
+ ->setPrice(10)
+ ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
+ ->setWebsiteIds([1])
+ ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1])
+ ->setProductLinks([$productLink])
+ ->save();
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php
new file mode 100644
index 0000000000000..958398660b132
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php
@@ -0,0 +1,33 @@
+get(\Magento\Framework\Registry::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+
+try {
+ $firstProduct = $productRepository->get('simple', false, null, true);
+ $productRepository->delete($firstProduct);
+} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
+ //Product already removed
+}
+
+try {
+ $secondProduct = $productRepository->get('simple_with_cross', false, null, true);
+ $productRepository->delete($secondProduct);
+} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
+ //Product already removed
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php
new file mode 100644
index 0000000000000..5cca93ce3478c
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php
@@ -0,0 +1,55 @@
+requireDataFixture('Magento/Customer/_files/customer.php');
+Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address.php');
+Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated.php');
+
+$objectManager = Bootstrap::getObjectManager();
+/** @var ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->get(ProductRepositoryInterface::class);
+$productRepository->cleanCache();
+/** @var CartRepositoryInterface $quoteRepository */
+$quoteRepository = $objectManager->get(CartRepositoryInterface::class);
+/** @var AddressInterface $quoteShippingAddress */
+$quoteShippingAddress = $objectManager->get(AddressInterfaceFactory::class)->create();
+/** @var CustomerRepositoryInterface $customerRepository */
+$customerRepository = $objectManager->get(CustomerRepositoryInterface::class);
+/** @var AddressRepositoryInterface $addressRepository */
+$addressRepository = $objectManager->get(AddressRepositoryInterface::class);
+$quoteShippingAddress->importCustomerAddressData($addressRepository->getById(1));
+$customer = $customerRepository->getById(1);
+
+/** @var CartInterface $quote */
+$quote = $objectManager->get(CartInterfaceFactory::class)->create();
+$quote->setStoreId(1)
+ ->setIsActive(true)
+ ->setIsMultiShipping(0)
+ ->assignCustomerWithAddressChange($customer)
+ ->setShippingAddress($quoteShippingAddress)
+ ->setBillingAddress($quoteShippingAddress)
+ ->setCheckoutMethod(Onepage::METHOD_CUSTOMER)
+ ->setReservedOrderId('55555555')
+ ->setEmail($customer->getEmail());
+$quote->addProduct($productRepository->get('simple-1'), 55);
+$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate');
+$quote->getShippingAddress()->setCollectShippingRates(true);
+$quote->getShippingAddress()->collectShippingRates();
+$quote->getPayment()->setMethod('checkmo');
+$quoteRepository->save($quote);
diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php
new file mode 100644
index 0000000000000..a599d008cf89c
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php
@@ -0,0 +1,26 @@
+get(CartRepositoryInterface::class);
+/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */
+$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class);
+$quote = $getQuoteByReservedOrderId->execute('55555555');
+if ($quote) {
+ $quoteRepository->delete($quote);
+}
+
+Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated_rollback.php');
+Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated_rollback.php');
+Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address_rollback.php');
+Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php');
diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.php
new file mode 100644
index 0000000000000..c74e76f74115f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.php
@@ -0,0 +1,39 @@
+requireDataFixture('Magento/Customer/_files/customer.php');
+Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product.php');
+
+$objectManager = Bootstrap::getObjectManager();
+/** @var ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->get(ProductRepositoryInterface::class);
+$productRepository->cleanCache();
+/** @var CartRepositoryInterface $quoteRepository */
+$quoteRepository = $objectManager->get(CartRepositoryInterface::class);
+/** @var CustomerRepositoryInterface $customerRepository */
+$customerRepository = $objectManager->get(CustomerRepositoryInterface::class);
+$customer = $customerRepository->get('customer@example.com');
+
+/** @var CartInterface $quote */
+$quote = $objectManager->get(CartInterfaceFactory::class)->create();
+$quote->setStoreId(1)
+ ->setIsActive(false)
+ ->setIsMultiShipping(0)
+ ->setCustomer($customer)
+ ->setCheckoutMethod(Onepage::METHOD_CUSTOMER)
+ ->setReservedOrderId('test_order_with_customer_inactive_quote')
+ ->addProduct($productRepository->get('taxable_product'), 1);
+$quoteRepository->save($quote);
diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.php
new file mode 100644
index 0000000000000..d45cbb547d29d
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.php
@@ -0,0 +1,24 @@
+get(CartRepositoryInterface::class);
+/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */
+$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class);
+$quote = $getQuoteByReservedOrderId->execute('test_order_with_customer_inactive_quote');
+if ($quote !== null) {
+ $quoteRepository->delete($quote);
+}
+
+Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product_rollback.php');
+Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php');
diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php
new file mode 100644
index 0000000000000..88c8fb726c472
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php
@@ -0,0 +1,108 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->serializer = $this->objectManager->get(SerializerInterface::class);
+ $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
+ $this->productRepository->cleanCache();
+ $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Configurable::class);
+ $this->registry = $this->objectManager->get(Registry::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->registry->unregister('product');
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php
+ * @return void
+ */
+ public function testGetProduct(): void
+ {
+ $product = $this->productRepository->get('simple-1');
+ $this->registerProduct($product);
+ $blockProduct = $this->block->getProduct();
+ $this->assertSame($product, $blockProduct);
+ $this->assertEquals(
+ $product->getId(),
+ $blockProduct->getId(),
+ 'The expected product is missing in the Configurable block!'
+ );
+ $this->assertNotNull($blockProduct->getTypeInstance()->getStoreFilter($blockProduct));
+ }
+
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php
+ * @return void
+ */
+ public function testGetJsonConfig(): void
+ {
+ $product = $this->productRepository->get('configurable');
+ $this->registerProduct($product);
+ $config = $this->serializer->unserialize($this->block->getJsonConfig());
+ $this->assertTrue($config['disablePriceReload']);
+ $this->assertTrue($config['stablePrices']);
+ }
+
+ /**
+ * Register the product
+ *
+ * @param ProductInterface $product
+ * @return void
+ */
+ private function registerProduct(ProductInterface $product): void
+ {
+ $this->registry->unregister('product');
+ $this->registry->register('product', $product);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php
index 55f8b91f07093..303a32d34bf6c 100644
--- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php
+++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php
@@ -12,7 +12,6 @@
/**
* Test cases related to check that configurable product custom option renders as expected.
*
- * @magentoDbIsolation disabled
* @magentoAppArea frontend
*/
class RenderOptionsTest extends AbstractRenderCustomOptionsTest
@@ -93,4 +92,20 @@ protected function getHandlesList(): array
'catalog_product_view_type_configurable',
];
}
+
+ /**
+ * @inheritdoc
+ */
+ protected function getMaxCharactersCssClass(): string
+ {
+ return 'class="character-counter';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getOptionsBlockName(): string
+ {
+ return 'product.info.options';
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php
index 39ed7965ea9e9..0344d467a3cc2 100644
--- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php
+++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php
@@ -9,9 +9,13 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\Catalog\Helper\Product as HelperProduct;
use Magento\Catalog\Model\ResourceModel\Product as ProductResource;
+use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute;
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\DataObject;
+use Magento\Framework\DataObjectFactory;
use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Framework\View\LayoutInterface;
@@ -26,6 +30,7 @@
* @magentoAppIsolation enabled
* @magentoDbIsolation enabled
* @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class ConfigurableTest extends TestCase
{
@@ -64,6 +69,14 @@ class ConfigurableTest extends TestCase
*/
private $product;
+ /**
+ * @var HelperProduct
+ */
+ private $helperProduct;
+
+ /** @var DataObjectFactory */
+ private $dataObjectFactory;
+
/**
* @inheritdoc
*/
@@ -79,6 +92,8 @@ protected function setUp(): void
$this->product = $this->productRepository->get('configurable');
$this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Configurable::class);
$this->block->setProduct($this->product);
+ $this->helperProduct = $this->objectManager->get(HelperProduct::class);
+ $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class);
}
/**
@@ -128,6 +143,29 @@ public function testGetJsonConfig(): void
$this->assertCount(0, $config['images']);
}
+ /**
+ * @return void
+ */
+ public function testGetJsonConfigWithPreconfiguredValues(): void
+ {
+ /** @var ConfigurableAttribute $attribute */
+ $attribute = $this->product->getExtensionAttributes()->getConfigurableProductOptions()[0];
+ $expectedAttributeValue = [
+ $attribute->getAttributeId() => $attribute->getOptions()[0]['value_index'],
+ ];
+ /** @var DataObject $request */
+ $buyRequest = $this->dataObjectFactory->create();
+ $buyRequest->setData([
+ 'qty' => 1,
+ 'super_attribute' => $expectedAttributeValue,
+ ]);
+ $this->helperProduct->prepareProductOptions($this->product, $buyRequest);
+
+ $config = $this->serializer->unserialize($this->block->getJsonConfig());
+ $this->assertArrayHasKey('defaultValues', $config);
+ $this->assertEquals($expectedAttributeValue, $config['defaultValues']);
+ }
+
/**
* @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_child_products_with_images.php
* @return void
diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php
index 9c382068ceebc..12585992d084c 100644
--- a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php
+++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php
@@ -3,126 +3,175 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Customer\Block\Address;
+use Magento\Customer\Model\AddressRegistry;
+use Magento\Customer\Model\CustomerRegistry;
+use Magento\Customer\Model\Session;
+use Magento\Framework\App\RequestInterface;
+use Magento\Framework\ObjectManagerInterface;
+use Magento\Framework\View\Result\Page;
+use Magento\Framework\View\Result\PageFactory;
+use Magento\TestFramework\Helper\Bootstrap;
+use Magento\TestFramework\Helper\Xpath;
+use PHPUnit\Framework\TestCase;
+
/**
* Tests Address Edit Block
+ *
+ * @magentoAppArea frontend
+ * @magentoAppIsolation enabled
*/
-class EditTest extends \PHPUnit\Framework\TestCase
+class EditTest extends TestCase
{
+ /** @var ObjectManagerInterface */
+ private $objectManager;
+
/** @var Edit */
- protected $_block;
+ private $block;
- /** @var \Magento\Customer\Model\Session */
- protected $_customerSession;
+ /** @var Session */
+ private $customerSession;
- /** @var \Magento\Backend\Block\Template\Context */
- protected $_context;
+ /** @var AddressRegistry */
+ private $addressRegistry;
- /** @var string */
- protected $_requestId;
+ /** @var CustomerRegistry */
+ private $customerRegistry;
+ /** @var RequestInterface */
+ private $request;
+
+ /**
+ * @inheritdoc
+ */
protected function setUp(): void
{
- $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
-
- $this->_customerSession = $objectManager->get(\Magento\Customer\Model\Session::class);
- $this->_customerSession->setCustomerId(1);
-
- $this->_context = $objectManager->get(\Magento\Backend\Block\Template\Context::class);
- $this->_requestId = $this->_context->getRequest()->getParam('id');
- $this->_context->getRequest()->setParam('id', '1');
-
- $objectManager->get(\Magento\Framework\App\State::class)->setAreaCode('frontend');
-
- /** @var $layout \Magento\Framework\View\Layout */
- $layout = $objectManager->get(\Magento\Framework\View\LayoutInterface::class);
- $currentCustomer = $objectManager->create(
- \Magento\Customer\Helper\Session\CurrentCustomer::class,
- ['customerSession' => $this->_customerSession]
- );
- $this->_block = $layout->createBlock(
- \Magento\Customer\Block\Address\Edit::class,
- '',
- ['customerSession' => $this->_customerSession, 'currentCustomer' => $currentCustomer]
- );
+ parent::setUp();
+ $this->objectManager = Bootstrap::getObjectManager();
+ $this->customerSession = $this->objectManager->get(Session::class);
+ $this->customerSession->setCustomerId(1);
+ $this->request = $this->objectManager->get(RequestInterface::class);
+ $this->request->setParam('id', '1');
+ /** @var Page $page */
+ $page = $this->objectManager->get(PageFactory::class)->create();
+ $page->addHandle(['default', 'customer_address_form']);
+ $page->getLayout()->generateXml();
+ $this->block = $page->getLayout()->getBlock('customer_address_edit');
+ $this->addressRegistry = $this->objectManager->get(AddressRegistry::class);
+ $this->customerRegistry = $this->objectManager->get(CustomerRegistry::class);
}
+ /**
+ * @inheritdoc
+ */
protected function tearDown(): void
{
- $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
- $this->_customerSession->setCustomerId(null);
- $this->_context->getRequest()->setParam('id', $this->_requestId);
- /** @var \Magento\Customer\Model\AddressRegistry $addressRegistry */
- $addressRegistry = $objectManager->get(\Magento\Customer\Model\AddressRegistry::class);
+ parent::tearDown();
+ $this->customerSession->setCustomerId(null);
+ $this->request->setParam('id', null);
//Cleanup address from registry
- $addressRegistry->remove(1);
- $addressRegistry->remove(2);
-
- /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */
- $customerRegistry = $objectManager->get(\Magento\Customer\Model\CustomerRegistry::class);
+ $this->addressRegistry->remove(1);
+ $this->addressRegistry->remove(2);
//Cleanup customer from registry
- $customerRegistry->remove(1);
+ $this->customerRegistry->remove(1);
}
/**
* @magentoDataFixture Magento/Customer/_files/customer.php
+ * @return void
*/
- public function testGetSaveUrl()
+ public function testGetSaveUrl(): void
{
- $this->assertEquals('http://localhost/index.php/customer/address/formPost/', $this->_block->getSaveUrl());
+ $this->assertEquals('http://localhost/index.php/customer/address/formPost/', $this->block->getSaveUrl());
}
/**
* @magentoDataFixture Magento/Customer/_files/customer.php
* @magentoDataFixture Magento/Customer/_files/customer_address.php
+ * @return void
*/
- public function testGetRegionId()
+ public function testGetRegionId(): void
{
- $this->assertEquals(1, $this->_block->getRegionId());
+ $this->assertEquals(1, $this->block->getRegionId());
}
/**
* @magentoDataFixture Magento/Customer/_files/customer.php
* @magentoDataFixture Magento/Customer/_files/customer_address.php
+ * @return void
*/
- public function testGetCountryId()
+ public function testGetCountryId(): void
{
- $this->assertEquals('US', $this->_block->getCountryId());
+ $this->assertEquals('US', $this->block->getCountryId());
}
/**
* @magentoDataFixture Magento/Customer/_files/customer.php
* @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php
+ * @return void
*/
- public function testGetCustomerAddressCount()
+ public function testGetCustomerAddressCount(): void
{
- $this->assertEquals(2, $this->_block->getCustomerAddressCount());
+ $this->assertEquals(2, $this->block->getCustomerAddressCount());
}
/**
* @magentoDataFixture Magento/Customer/_files/customer.php
+ * @return void
*/
- public function testCanSetAsDefaultShipping()
+ public function testCanSetAsDefaultShipping(): void
{
- $this->assertEquals(0, $this->_block->canSetAsDefaultShipping());
+ $this->assertEquals(0, $this->block->canSetAsDefaultShipping());
}
/**
* @magentoDataFixture Magento/Customer/_files/customer.php
+ * @return void
*/
- public function testIsDefaultBilling()
+ public function testIsDefaultBilling(): void
{
- $this->assertFalse($this->_block->isDefaultBilling());
+ $this->assertFalse($this->block->isDefaultBilling());
}
/**
* @magentoDataFixture Magento/Customer/_files/customer.php
* @magentoDataFixture Magento/Customer/_files/customer_address.php
+ * @return void
+ */
+ public function testGetStreetLine(): void
+ {
+ $this->assertEquals('Green str, 67', $this->block->getStreetLine(1));
+ $this->assertEquals('', $this->block->getStreetLine(2));
+ }
+
+ /**
+ * @magentoDataFixture Magento/Customer/_files/customer.php
+ * @magentoConfigFixture current_store customer/create_account/vat_frontend_visibility 1
+ * @return void
+ */
+ public function testVatIdFieldVisible(): void
+ {
+ $html = $this->block->toHtml();
+ $labelXpath = "//div[contains(@class, 'taxvat')]//label/span[normalize-space(text()) = '%s']";
+ $this->assertEquals(1, Xpath::getElementsCountForXpath(sprintf($labelXpath, __('VAT Number')), $html));
+ $inputXpath = "//div[contains(@class, 'taxvat')]//div/input[contains(@id,'vat_id') and @type='text']";
+ $this->assertEquals(1, Xpath::getElementsCountForXpath($inputXpath, $html));
+ }
+
+ /**
+ * @magentoDataFixture Magento/Customer/_files/customer.php
+ * @magentoConfigFixture current_store customer/create_account/vat_frontend_visibility 0
+ * @return void
*/
- public function testGetStreetLine()
+ public function testVatIdFieldNotVisible(): void
{
- $this->assertEquals('Green str, 67', $this->_block->getStreetLine(1));
- $this->assertEquals('', $this->_block->getStreetLine(2));
+ $html = $this->block->toHtml();
+ $labelXpath = "//div[contains(@class, 'taxvat')]//label/span[normalize-space(text()) = '%s']";
+ $this->assertEquals(0, Xpath::getElementsCountForXpath(sprintf($labelXpath, __('VAT Number')), $html));
+ $inputXpath = "//div[contains(@class, 'taxvat')]//div/input[contains(@id,'vat_id') and @type='text']";
+ $this->assertEquals(0, Xpath::getElementsCountForXpath($inputXpath, $html));
}
}
diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php
new file mode 100644
index 0000000000000..8ce1d2ae9ccf9
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php
@@ -0,0 +1,303 @@
+transportBuilderMock = $this->_objectManager->get(TransportBuilderMock::class);
+ $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class);
+ $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class);
+ $this->customerRegistry = $this->_objectManager->get(CustomerRegistry::class);
+ $this->cookieManager = $this->_objectManager->get(CookieManagerInterface::class);
+ $this->urlBuilder = $this->_objectManager->get(UrlInterface::class);
+ }
+
+ /**
+ * Tests that without form key user account won't be created
+ * and user will be redirected on account creation page again.
+ *
+ * @magentoDbIsolation enabled
+ * @magentoAppIsolation enabled
+ * @return void
+ */
+ public function testNoFormKeyCreatePostAction(): void
+ {
+ $this->fillRequestWithAccountData('test1@email.com');
+ $this->getRequest()->setPostValue('form_key', null);
+ $this->dispatch('customer/account/createPost');
+
+ $this->assertCustomerNotExists('test1@email.com');
+ $this->assertRedirect($this->stringEndsWith('customer/account/create/'));
+ $this->assertSessionMessages(
+ $this->stringContains((string)__('Invalid Form Key. Please refresh the page.')),
+ MessageInterface::TYPE_ERROR
+ );
+ }
+
+ /**
+ * @magentoDbIsolation enabled
+ * @magentoAppIsolation enabled
+ * @magentoConfigFixture current_website customer/create_account/confirm 0
+ * @magentoConfigFixture current_store customer/create_account/default_group 1
+ * @magentoConfigFixture current_store customer/create_account/generate_human_friendly_id 0
+ *
+ * @return void
+ */
+ public function testNoConfirmCreatePostAction(): void
+ {
+ $this->fillRequestWithAccountData('test1@email.com');
+ $this->dispatch('customer/account/createPost');
+ $this->assertRedirect($this->stringEndsWith('customer/account/'));
+ $this->assertSessionMessages(
+ $this->containsEqual(
+ (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName())
+ ),
+ MessageInterface::TYPE_SUCCESS
+ );
+ $customer = $this->customerRegistry->retrieveByEmail('test1@email.com');
+ //Assert customer group
+ $this->assertEquals(1, $customer->getDataModel()->getGroupId());
+ //Assert customer increment id generation
+ $this->assertNull($customer->getData('increment_id'));
+ }
+
+ /**
+ * @magentoDbIsolation enabled
+ * @magentoAppIsolation enabled
+ * @magentoConfigFixture current_website customer/create_account/confirm 0
+ * @magentoConfigFixture current_store customer/create_account/default_group 2
+ * @magentoConfigFixture current_store customer/create_account/generate_human_friendly_id 1
+ * @return void
+ */
+ public function testCreatePostWithCustomConfiguration(): void
+ {
+ $this->fillRequestWithAccountData('test@email.com');
+ $this->dispatch('customer/account/createPost');
+ $this->assertRedirect($this->stringEndsWith('customer/account/'));
+ $this->assertSessionMessages(
+ $this->containsEqual(
+ (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName())
+ ),
+ MessageInterface::TYPE_SUCCESS
+ );
+ $customer = $this->customerRegistry->retrieveByEmail('test@email.com');
+ //Assert customer group
+ $this->assertEquals(2, $customer->getDataModel()->getGroupId());
+ //Assert customer increment id generation
+ $this->assertNotNull($customer->getData('increment_id'));
+ $this->assertMatchesRegularExpression('/\d{8}/', $customer->getData('increment_id'));
+ }
+
+ /**
+ * @magentoDbIsolation enabled
+ * @magentoAppIsolation enabled
+ * @magentoConfigFixture current_website customer/create_account/confirm 1
+ *
+ * @return void
+ */
+ public function testWithConfirmCreatePostAction(): void
+ {
+ $email = 'test2@email.com';
+ $this->fillRequestWithAccountData($email);
+ $this->dispatch('customer/account/createPost');
+ $this->assertRedirect($this->stringContains('customer/account/index/'));
+ $message = 'You must confirm your account.'
+ . ' Please check your email for the confirmation link or click here for a new link.';
+ $url = $this->urlBuilder->getUrl('customer/account/confirmation', ['_query' => ['email' => $email]]);
+ $this->assertSessionMessages(
+ $this->containsEqual((string)__($message, $url)),
+ MessageInterface::TYPE_SUCCESS
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/Customer/_files/customer.php
+ *
+ * @return void
+ */
+ public function testExistingEmailCreatePostAction(): void
+ {
+ $this->fillRequestWithAccountData('customer@example.com');
+ $this->dispatch('customer/account/createPost');
+ $this->assertRedirect($this->stringContains('customer/account/create/'));
+ $message = 'There is already an account with this email address.'
+ . ' If you are sure that it is your email address, click here '
+ . 'to get your password and access your account.';
+ $url = $this->urlBuilder->getUrl('customer/account/forgotpassword');
+ $this->assertSessionMessages($this->containsEqual((string)__($message, $url)), MessageInterface::TYPE_ERROR);
+ }
+
+ /**
+ * Register Customer with email confirmation.
+ *
+ * @magentoAppArea frontend
+ * @magentoConfigFixture current_website customer/create_account/confirm 1
+ *
+ * @return void
+ */
+ public function testRegisterCustomerWithEmailConfirmation(): void
+ {
+ $email = 'test_example@email.com';
+ $this->fillRequestWithAccountData($email);
+ $this->dispatch('customer/account/createPost');
+ $this->assertRedirect($this->stringContains('customer/account/index/'));
+ $message = 'You must confirm your account.'
+ . ' Please check your email for the confirmation link or click here for a new link.';
+ $url = $this->urlBuilder->getUrl('customer/account/confirmation', ['_query' => ['email' => $email]]);
+ $this->assertSessionMessages($this->containsEqual((string)__($message, $url)), MessageInterface::TYPE_SUCCESS);
+ /** @var CustomerInterface $customer */
+ $customer = $this->customerRepository->get($email);
+ $confirmation = $customer->getConfirmation();
+ $sendMessage = $this->transportBuilderMock->getSentMessage();
+ $this->assertNotNull($sendMessage);
+ $rawMessage = $sendMessage->getBody()->getParts()[0]->getRawContent();
+ $this->assertStringContainsString(
+ (string)__(
+ 'You must confirm your %customer_email email before you can sign in (link is only valid once):',
+ ['customer_email' => $email]
+ ),
+ $rawMessage
+ );
+ $this->assertStringContainsString(
+ sprintf('customer/account/confirm/?id=%s&key=%s', $customer->getId(), $confirmation),
+ $rawMessage
+ );
+ $this->resetRequest();
+ $this->getRequest()
+ ->setParam('id', $customer->getId())
+ ->setParam('key', $confirmation);
+ $this->dispatch('customer/account/confirm');
+ $this->assertRedirect($this->stringContains('customer/account/index/'));
+ $this->assertSessionMessages(
+ $this->containsEqual(
+ (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName())
+ ),
+ MessageInterface::TYPE_SUCCESS
+ );
+ $this->assertEmpty($this->customerRepository->get($email)->getConfirmation());
+ }
+
+ /**
+ * Fills request with customer data.
+ *
+ * @param string $email
+ * @return void
+ */
+ private function fillRequestWithAccountData(string $email): void
+ {
+ $this->getRequest()
+ ->setMethod(HttpRequest::METHOD_POST)
+ ->setParam(CustomerInterface::FIRSTNAME, 'firstname1')
+ ->setParam(CustomerInterface::LASTNAME, 'lastname1')
+ ->setParam(CustomerInterface::EMAIL, $email)
+ ->setParam('password', '_Password1')
+ ->setParam('password_confirmation', '_Password1')
+ ->setParam('telephone', '5123334444')
+ ->setParam('street', ['1234 fake street', ''])
+ ->setParam('city', 'Austin')
+ ->setParam('postcode', '78701')
+ ->setParam('country_id', 'US')
+ ->setParam('default_billing', '1')
+ ->setParam('default_shipping', '1')
+ ->setParam('is_subscribed', '0')
+ ->setPostValue('create_address', true);
+ }
+
+ /**
+ * Asserts that customer does not exists.
+ *
+ * @param string $email
+ * @return void
+ */
+ private function assertCustomerNotExists(string $email): void
+ {
+ $this->expectException(NoSuchEntityException::class);
+ $this->expectExceptionMessage(
+ (string)__(
+ 'No such entity with %fieldName = %fieldValue, %field2Name = %field2Value',
+ [
+ 'fieldName' => 'email',
+ 'fieldValue' => $email,
+ 'field2Name' => 'websiteId',
+ 'field2Value' => 1
+ ]
+ )
+ );
+ $this->assertNull($this->customerRepository->get($email));
+ }
+
+ /**
+ * Clears request.
+ *
+ * @return void
+ */
+ protected function resetRequest(): void
+ {
+ parent::resetRequest();
+ $this->cookieManager->deleteCookie(MessagePlugin::MESSAGES_COOKIES_NAME);
+ $this->_objectManager->removeSharedInstance(Http::class);
+ $this->_objectManager->removeSharedInstance(Request::class);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php
index 5527a39ce0507..6abbff18c645c 100644
--- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php
+++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php
@@ -10,13 +10,10 @@
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Customer\Model\CustomerRegistry;
use Magento\Customer\Model\Session;
-use Magento\Framework\Api\FilterBuilder;
-use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\App\Http;
use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\Framework\Data\Form\FormKey;
use Magento\Framework\Message\MessageInterface;
-use Magento\Framework\Phrase;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\Stdlib\CookieManagerInterface;
use Magento\Store\Model\StoreManager;
@@ -220,83 +217,6 @@ public function testConfirmActionAlreadyActive()
$this->getResponse()->getBody();
}
- /**
- * Tests that without form key user account won't be created
- * and user will be redirected on account creation page again.
- */
- public function testNoFormKeyCreatePostAction()
- {
- $this->fillRequestWithAccountData('test1@email.com');
- $this->getRequest()->setPostValue('form_key', null);
- $this->dispatch('customer/account/createPost');
-
- $this->assertNull($this->getCustomerByEmail('test1@email.com'));
- $this->assertRedirect($this->stringEndsWith('customer/account/create/'));
- $this->assertSessionMessages(
- $this->equalTo([new Phrase('Invalid Form Key. Please refresh the page.')]),
- MessageInterface::TYPE_ERROR
- );
- }
-
- /**
- * @magentoDbIsolation enabled
- * @magentoAppIsolation enabled
- * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_disable.php
- */
- public function testNoConfirmCreatePostAction()
- {
- $this->fillRequestWithAccountDataAndFormKey('test1@email.com');
- $this->dispatch('customer/account/createPost');
- $this->assertRedirect($this->stringEndsWith('customer/account/'));
- $this->assertSessionMessages(
- $this->equalTo(['Thank you for registering with Main Website Store.']),
- MessageInterface::TYPE_SUCCESS
- );
- }
-
- /**
- * @magentoDbIsolation enabled
- * @magentoAppIsolation enabled
- * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php
- */
- public function testWithConfirmCreatePostAction()
- {
- $this->fillRequestWithAccountDataAndFormKey('test2@email.com');
- $this->dispatch('customer/account/createPost');
- $this->assertRedirect($this->stringContains('customer/account/index/'));
- $this->assertSessionMessages(
- $this->equalTo(
- [
- 'You must confirm your account. Please check your email for the confirmation link or '
- . ' click here for a new link.'
- ]
- ),
- MessageInterface::TYPE_SUCCESS
- );
- }
-
- /**
- * @magentoDataFixture Magento/Customer/_files/customer.php
- */
- public function testExistingEmailCreatePostAction()
- {
- $this->fillRequestWithAccountDataAndFormKey('customer@example.com');
- $this->dispatch('customer/account/createPost');
- $this->assertRedirect($this->stringContains('customer/account/create/'));
- $this->assertSessionMessages(
- $this->equalTo(
- [
- 'There is already an account with this email address. ' .
- 'If you are sure that it is your email address, ' .
- ' click here' .
- ' to get your password and access your account.',
- ]
- ),
- MessageInterface::TYPE_ERROR
- );
- }
-
/**
* @magentoDataFixture Magento/Customer/_files/inactive_customer.php
*/
@@ -613,70 +533,6 @@ public function testWrongConfirmationEditPostAction()
);
}
- /**
- * Register Customer with email confirmation.
- *
- * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php
- * @return void
- * @throws \Magento\Framework\Exception\InputException
- * @throws \Magento\Framework\Exception\LocalizedException
- * @throws \Magento\Framework\Exception\NoSuchEntityException
- * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException
- */
- public function testRegisterCustomerWithEmailConfirmation(): void
- {
- $email = 'test_example@email.com';
- $this->fillRequestWithAccountDataAndFormKey($email);
- $this->dispatch('customer/account/createPost');
- $this->assertRedirect($this->stringContains('customer/account/index/'));
- $this->assertSessionMessages(
- $this->equalTo(
- [
- 'You must confirm your account. Please check your email for the confirmation link or '
- . ' click here for a new link.'
- ]
- ),
- MessageInterface::TYPE_SUCCESS
- );
- /** @var CustomerRepositoryInterface $customerRepository */
- $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class);
- /** @var CustomerInterface $customer */
- $customer = $customerRepository->get($email);
- $confirmation = $customer->getConfirmation();
- $message = $this->transportBuilderMock->getSentMessage();
- $rawMessage = $message->getBody()->getParts()[0]->getRawContent();
- $messageConstraint = $this->logicalAnd(
- new StringContains("You must confirm your {$email} email before you can sign in (link is only valid once"),
- new StringContains("customer/account/confirm/?id={$customer->getId()}&key={$confirmation}")
- );
- $this->assertThat($rawMessage, $messageConstraint);
-
- /** @var CookieManagerInterface $cookieManager */
- $cookieManager = $this->_objectManager->get(CookieManagerInterface::class);
- $cookieManager->deleteCookie(MessagePlugin::MESSAGES_COOKIES_NAME);
-
- $this->_objectManager->removeSharedInstance(Http::class);
- $this->_objectManager->removeSharedInstance(Request::class);
- $this->_request = null;
-
- $this->getRequest()
- ->setParam('id', $customer->getId())
- ->setParam('key', $confirmation);
- $this->dispatch('customer/account/confirm');
-
- /** @var StoreManager $store */
- $store = $this->_objectManager->get(StoreManagerInterface::class);
- $name = $store->getStore()->getFrontendName();
-
- $this->assertRedirect($this->stringContains('customer/account/index/'));
- $this->assertSessionMessages(
- $this->equalTo(["Thank you for registering with {$name}."]),
- MessageInterface::TYPE_SUCCESS
- );
- $this->assertEmpty($customerRepository->get($email)->getConfirmation());
- }
-
/**
* Test that confirmation email address displays special characters correctly.
*
@@ -867,74 +723,6 @@ protected function resetRequest(): void
parent::resetRequest();
}
- /**
- * @param string $email
- * @return void
- */
- private function fillRequestWithAccountData($email)
- {
- $this->getRequest()
- ->setMethod('POST')
- ->setParam('firstname', 'firstname1')
- ->setParam('lastname', 'lastname1')
- ->setParam('company', '')
- ->setParam('email', $email)
- ->setParam('password', '_Password1')
- ->setParam('password_confirmation', '_Password1')
- ->setParam('telephone', '5123334444')
- ->setParam('street', ['1234 fake street', ''])
- ->setParam('city', 'Austin')
- ->setParam('region_id', 57)
- ->setParam('region', '')
- ->setParam('postcode', '78701')
- ->setParam('country_id', 'US')
- ->setParam('default_billing', '1')
- ->setParam('default_shipping', '1')
- ->setParam('is_subscribed', '0')
- ->setPostValue('create_address', true);
- }
-
- /**
- * @param string $email
- * @return void
- */
- private function fillRequestWithAccountDataAndFormKey($email)
- {
- $this->fillRequestWithAccountData($email);
- $formKey = $this->_objectManager->get(FormKey::class);
- $this->getRequest()->setParam('form_key', $formKey->getFormKey());
- }
-
- /**
- * Returns stored customer by email.
- *
- * @param string $email
- * @return CustomerInterface
- */
- private function getCustomerByEmail($email)
- {
- /** @var FilterBuilder $filterBuilder */
- $filterBuilder = $this->_objectManager->get(FilterBuilder::class);
- $filters = [
- $filterBuilder->setField(CustomerInterface::EMAIL)
- ->setValue($email)
- ->create()
- ];
-
- /** @var SearchCriteriaBuilder $searchCriteriaBuilder */
- $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class);
- $searchCriteria = $searchCriteriaBuilder->addFilters($filters)
- ->create();
-
- $customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class);
- $customers = $customerRepository->getList($searchCriteria)
- ->getItems();
-
- $customer = array_pop($customers);
-
- return $customer;
- }
-
/**
* Add new request info (request uri, path info, action name).
*
diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php
index e12068ef62b21..bd2c26e449d72 100644
--- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php
+++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php
@@ -13,9 +13,12 @@
use Magento\Customer\Api\Data\CustomerInterfaceFactory;
use Magento\Customer\Model\Customer;
use Magento\Customer\Model\CustomerFactory;
+use Magento\Customer\Model\EmailNotification;
+use Magento\Email\Model\ResourceModel\Template\CollectionFactory as TemplateCollectionFactory;
use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\Api\ExtensibleDataObjectConverter;
use Magento\Framework\Api\SimpleDataObjectConverter;
+use Magento\Framework\App\Config\MutableScopeConfigInterface;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\LocalizedException;
@@ -23,6 +26,7 @@
use Magento\Framework\Math\Random;
use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\Validator\Exception;
+use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\Helper\Xpath;
@@ -101,6 +105,16 @@ class CreateAccountTest extends TestCase
*/
private $encryptor;
+ /**
+ * @var MutableScopeConfigInterface
+ */
+ private $mutableScopeConfig;
+
+ /**
+ * @var TemplateCollectionFactory
+ */
+ private $templateCollectionFactory;
+
/**
* @inheritdoc
*/
@@ -117,9 +131,20 @@ protected function setUp(): void
$this->customerModelFactory = $this->objectManager->get(CustomerFactory::class);
$this->random = $this->objectManager->get(Random::class);
$this->encryptor = $this->objectManager->get(EncryptorInterface::class);
+ $this->mutableScopeConfig = $this->objectManager->get(MutableScopeConfigInterface::class);
+ $this->templateCollectionFactory = $this->objectManager->get(TemplateCollectionFactory::class);
parent::setUp();
}
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ $this->mutableScopeConfig->clean();
+ }
+
/**
* @dataProvider createInvalidAccountDataProvider
* @param array $customerData
@@ -220,6 +245,98 @@ public function createInvalidAccountDataProvider(): array
];
}
+ /**
+ * @magentoAppArea frontend
+ * @magentoDataFixture Magento/Customer/_files/customer_welcome_email_template.php
+ * @return void
+ */
+ public function testCreateAccountWithConfiguredWelcomeEmail(): void
+ {
+ $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_template');
+ $this->setConfig([EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE => $emailTemplate,]);
+ $this->accountManagement->createAccount(
+ $this->populateCustomerEntity($this->defaultCustomerData),
+ '_Password1'
+ );
+ $this->assertEmailData(
+ [
+ 'name' => 'Owner',
+ 'email' => 'owner@example.com',
+ 'message' => 'Customer create account email template',
+ ]
+ );
+ }
+
+ /**
+ * @magentoAppArea frontend
+ * @magentoDataFixture Magento/Customer/_files/customer_welcome_no_password_email_template.php
+ * @magentoConfigFixture current_store customer/create_account/email_identity support
+ * @return void
+ */
+ public function testCreateAccountWithConfiguredWelcomeNoPasswordEmail(): void
+ {
+ $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_no_password_template');
+ $this->setConfig([EmailNotification::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE => $emailTemplate,]);
+ $this->accountManagement->createAccount($this->populateCustomerEntity($this->defaultCustomerData));
+ $this->assertEmailData(
+ [
+ 'name' => 'CustomerSupport',
+ 'email' => 'support@example.com',
+ 'message' => 'Customer create account email no password template',
+ ]
+ );
+ }
+
+ /**
+ * @magentoAppArea frontend
+ * @magentoDataFixture Magento/Customer/_files/customer_confirmation_email_template.php
+ * @magentoConfigFixture current_website customer/create_account/confirm 1
+ * @magentoConfigFixture current_store customer/create_account/email_identity custom1
+ * @return void
+ */
+ public function testCreateAccountWithConfiguredConfirmationEmail(): void
+ {
+ $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_confirmation_template');
+ $this->setConfig([EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE => $emailTemplate,]);
+ $this->accountManagement->createAccount(
+ $this->populateCustomerEntity($this->defaultCustomerData),
+ '_Password1'
+ );
+ $this->assertEmailData(
+ [
+ 'name' => 'Custom 1',
+ 'email' => 'custom1@example.com',
+ 'message' => 'Customer create account email confirmation template',
+ ]
+ );
+ }
+
+ /**
+ * @magentoAppArea frontend
+ * @magentoDataFixture Magento/Customer/_files/customer_confirmed_email_template.php
+ * @magentoConfigFixture current_store customer/create_account/email_identity custom1
+ * @magentoConfigFixture current_website customer/create_account/confirm 1
+ * @return void
+ */
+ public function testCreateAccountWithConfiguredConfirmedEmail(): void
+ {
+ $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_confirmed_template');
+ $this->setConfig([EmailNotification::XML_PATH_CONFIRMED_EMAIL_TEMPLATE => $emailTemplate,]);
+ $this->accountManagement->createAccount(
+ $this->populateCustomerEntity($this->defaultCustomerData),
+ '_Password1'
+ );
+ $customer = $this->customerRepository->get('customer@example.com');
+ $this->accountManagement->activate($customer->getEmail(), $customer->getConfirmation());
+ $this->assertEmailData(
+ [
+ 'name' => 'Custom 1',
+ 'email' => 'custom1@example.com',
+ 'message' => 'Customer create account email confirmed template',
+ ]
+ );
+ }
+
/**
* Assert that when you create customer account via admin, link with "set password" is send to customer email.
*
@@ -589,4 +706,53 @@ private function assertCustomerData(
);
}
}
+
+ /**
+ * Sets config data.
+ *
+ * @param array $configs
+ * @return void
+ */
+ private function setConfig(array $configs): void
+ {
+ foreach ($configs as $path => $value) {
+ $this->mutableScopeConfig->setValue($path, $value, ScopeInterface::SCOPE_STORE, 'default');
+ }
+ }
+
+ /**
+ * Assert email data.
+ *
+ * @param array $expectedData
+ * @return void
+ */
+ private function assertEmailData(array $expectedData): void
+ {
+ $message = $this->transportBuilderMock->getSentMessage();
+ $this->assertNotNull($message);
+ $messageFrom = $message->getFrom();
+ $this->assertNotNull($messageFrom);
+ $messageFrom = reset($messageFrom);
+ $this->assertEquals($expectedData['name'], $messageFrom->getName());
+ $this->assertEquals($expectedData['email'], $messageFrom->getEmail());
+ $this->assertStringContainsString(
+ $expectedData['message'],
+ $message->getBody()->getParts()[0]->getRawContent(),
+ 'Expected message wasn\'t found in email content.'
+ );
+ }
+
+ /**
+ * Returns email template id by template code.
+ *
+ * @param string $templateCode
+ * @return int
+ */
+ private function getCustomTemplateId(string $templateCode): int
+ {
+ return (int)$this->templateCollectionFactory->create()
+ ->addFieldToFilter('template_code', $templateCode)
+ ->getFirstItem()
+ ->getId();
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php
index ac55f93bc9e4b..eb638eeb329aa 100644
--- a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php
+++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php
@@ -14,11 +14,16 @@
use Magento\Customer\Model\AddressRegistry;
use Magento\Customer\Model\CustomerRegistry;
use Magento\Customer\Model\ResourceModel\Address;
+use Magento\Customer\Model\Vat;
+use Magento\Customer\Observer\AfterAddressSaveObserver;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\DataObjectFactory;
use Magento\Framework\Exception\InputException;
use Magento\TestFramework\Directory\Model\GetRegionIdByName;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\ObjectManager;
use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface as PsrLogger;
/**
* Assert that address was created as expected or address create throws expected error.
@@ -88,6 +93,11 @@ class CreateAddressTest extends TestCase
*/
private $createdAddressesIds = [];
+ /**
+ * @var DataObjectFactory
+ */
+ private $dataObjectFactory;
+
/**
* @inheritdoc
*/
@@ -101,6 +111,7 @@ protected function setUp(): void
$this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class);
$this->addressRegistry = $this->objectManager->get(AddressRegistry::class);
$this->addressResource = $this->objectManager->get(Address::class);
+ $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class);
parent::setUp();
}
@@ -112,6 +123,7 @@ protected function tearDown(): void
foreach ($this->createdAddressesIds as $createdAddressesId) {
$this->addressRegistry->remove($createdAddressesId);
}
+ $this->objectManager->removeSharedInstance(AfterAddressSaveObserver::class);
parent::tearDown();
}
@@ -326,6 +338,92 @@ public function createWrongAddressesDataProvider(): array
];
}
+ /**
+ * Assert that after address creation customer group is Group for Valid VAT ID - Domestic.
+ *
+ * @magentoAppIsolation enabled
+ * @magentoDataFixture Magento/Customer/_files/customer_no_address.php
+ * @magentoConfigFixture current_store general/store_information/country_id AT
+ * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1
+ * @magentoConfigFixture current_store customer/create_account/viv_domestic_group 2
+ * @return void
+ */
+ public function testAddressCreatedWithGroupAssignByDomesticVatId(): void
+ {
+ $this->createVatMock(true, true);
+ $addressData = array_merge(
+ self::STATIC_CUSTOMER_ADDRESS_DATA,
+ [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT']
+ );
+ $customer = $this->customerRepository->get('customer5@example.com');
+ $this->createAddress((int)$customer->getId(), $addressData, false, true);
+ $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com'));
+ }
+
+ /**
+ * Assert that after address creation customer group is Group for Valid VAT ID - Intra-Union.
+ *
+ * @magentoAppIsolation enabled
+ * @magentoDataFixture Magento/Customer/_files/customer_no_address.php
+ * @magentoConfigFixture current_store general/store_information/country_id GR
+ * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1
+ * @magentoConfigFixture current_store customer/create_account/viv_intra_union_group 2
+ * @return void
+ */
+ public function testAddressCreatedWithGroupAssignByIntraUnionVatId(): void
+ {
+ $this->createVatMock(true, true);
+ $addressData = array_merge(
+ self::STATIC_CUSTOMER_ADDRESS_DATA,
+ [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT']
+ );
+ $customer = $this->customerRepository->get('customer5@example.com');
+ $this->createAddress((int)$customer->getId(), $addressData, false, true);
+ $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com'));
+ }
+
+ /**
+ * Assert that after address creation customer group is Group for Invalid VAT ID.
+ *
+ * @magentoAppIsolation enabled
+ * @magentoDataFixture Magento/Customer/_files/customer_no_address.php
+ * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1
+ * @magentoConfigFixture current_store customer/create_account/viv_invalid_group 2
+ * @return void
+ */
+ public function testAddressCreatedWithGroupAssignByInvalidVatId(): void
+ {
+ $this->createVatMock(false, true);
+ $addressData = array_merge(
+ self::STATIC_CUSTOMER_ADDRESS_DATA,
+ [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT']
+ );
+ $customer = $this->customerRepository->get('customer5@example.com');
+ $this->createAddress((int)$customer->getId(), $addressData, false, true);
+ $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com'));
+ }
+
+ /**
+ * Assert that after address creation customer group is Validation Error Group.
+ *
+ * @magentoAppIsolation enabled
+ * @magentoDataFixture Magento/Customer/_files/customer_no_address.php
+ * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1
+ * @magentoConfigFixture current_store customer/create_account/viv_error_group 2
+ * @return void
+ */
+ public function testAddressCreatedWithGroupAssignByVatIdWithError(): void
+ {
+ $this->createVatMock(false, false);
+ $addressData = array_merge(
+ self::STATIC_CUSTOMER_ADDRESS_DATA,
+ [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT']
+ );
+ $customer = $this->customerRepository->get('customer5@example.com');
+ $this->createAddress((int)$customer->getId(), $addressData, false, true);
+ $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com'));
+ }
+
/**
* Create customer address with provided address data.
*
@@ -361,4 +459,49 @@ protected function createAddress(
return $address;
}
+
+ /**
+ * Creates mock for vat id validation.
+ *
+ * @param bool $isValid
+ * @param bool $isRequestSuccess
+ * @return void
+ */
+ private function createVatMock(bool $isValid = false, bool $isRequestSuccess = false): void
+ {
+ $gatewayResponse = $this->dataObjectFactory->create(
+ [
+ 'data' => [
+ 'is_valid' => $isValid,
+ 'request_date' => '',
+ 'request_identifier' => '123123123',
+ 'request_success' => $isRequestSuccess,
+ 'request_message' => __(''),
+ ],
+ ]
+ );
+ $customerVat = $this->getMockBuilder(Vat::class)
+ ->setConstructorArgs(
+ [
+ $this->objectManager->get(ScopeConfigInterface::class),
+ $this->objectManager->get(PsrLogger::class)
+ ]
+ )
+ ->setMethods(['checkVatNumber'])
+ ->getMock();
+ $customerVat->method('checkVatNumber')->willReturn($gatewayResponse);
+ $this->objectManager->removeSharedInstance(Vat::class);
+ $this->objectManager->addSharedInstance($customerVat, Vat::class);
+ }
+
+ /**
+ * Returns customer group id by email.
+ *
+ * @param string $email
+ * @return int
+ */
+ private function getCustomerGroupId(string $email): int
+ {
+ return (int)$this->customerRepository->get($email)->getGroupId();
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php
new file mode 100644
index 0000000000000..38b607230cbaf
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php
@@ -0,0 +1,30 @@
+get(TemplateResource::class);
+/** @var TemplateInterfaceFactory $templateFactory */
+$templateFactory = $objectManager->get(TemplateInterfaceFactory::class);
+/** @var TemplateInterface $template */
+$template = $templateFactory->create();
+
+$content = <<{{trans "Customer create account email confirmation template"}}
+{{template config_path="design/email/footer_template"}}
+HTML;
+
+$template->setTemplateCode('customer_create_account_email_confirmation_template')
+ ->setTemplateText($content)
+ ->setTemplateType(TemplateInterface::TYPE_HTML);
+$templateResource->save($template);
diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_rollback.php
new file mode 100644
index 0000000000000..07fee6e81fe47
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_rollback.php
@@ -0,0 +1,25 @@
+get(TemplateResource::class);
+/** @var Collection $collection */
+$collection = $objectManager->get(CollectionFactory::class)->create();
+/** @var TemplateInterface $template */
+$template = $collection
+ ->addFieldToFilter('template_code', 'customer_create_account_email_confirmation_template')
+ ->getFirstItem();
+if ($template->getId()) {
+ $templateResource->delete($template);
+}
diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php
new file mode 100644
index 0000000000000..859cae92dbd27
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php
@@ -0,0 +1,30 @@
+get(TemplateResource::class);
+/** @var TemplateInterfaceFactory $templateFactory */
+$templateFactory = $objectManager->get(TemplateInterfaceFactory::class);
+/** @var TemplateInterface $template */
+$template = $templateFactory->create();
+
+$content = <<{{trans "Customer create account email confirmed template"}}
+{{template config_path="design/email/footer_template"}}
+HTML;
+
+$template->setTemplateCode('customer_create_account_email_confirmed_template')
+ ->setTemplateText($content)
+ ->setTemplateType(TemplateInterface::TYPE_HTML);
+$templateResource->save($template);
diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_rollback.php
new file mode 100644
index 0000000000000..a4e03038d45bd
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_rollback.php
@@ -0,0 +1,25 @@
+get(TemplateResource::class);
+/** @var Collection $collection */
+$collection = $objectManager->get(CollectionFactory::class)->create();
+/** @var TemplateInterface $template */
+$template = $collection
+ ->addFieldToFilter('template_code', 'customer_create_account_email_confirmed_template')
+ ->getFirstItem();
+if ($template->getId()) {
+ $templateResource->delete($template);
+}
diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php
new file mode 100644
index 0000000000000..6cc273dbe235a
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php
@@ -0,0 +1,30 @@
+get(TemplateResource::class);
+/** @var TemplateInterfaceFactory $templateFactory */
+$templateFactory = $objectManager->get(TemplateInterfaceFactory::class);
+/** @var TemplateInterface $template */
+$template = $templateFactory->create();
+
+$content = <<{{trans "Customer create account email template"}}
+{{template config_path="design/email/footer_template"}}
+HTML;
+
+$template->setTemplateCode('customer_create_account_email_template')
+ ->setTemplateText($content)
+ ->setTemplateType(TemplateInterface::TYPE_HTML);
+$templateResource->save($template);
diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_rollback.php
new file mode 100644
index 0000000000000..6bef9822d3e9a
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_rollback.php
@@ -0,0 +1,23 @@
+get(TemplateResource::class);
+/** @var Collection $collection */
+$collection = $objectManager->get(CollectionFactory::class)->create();
+/** @var TemplateInterface $template */
+$template = $collection->addFieldToFilter('template_code', 'customer_create_account_email_template')->getFirstItem();
+if ($template->getId()) {
+ $templateResource->delete($template);
+}
diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php
new file mode 100644
index 0000000000000..a936bb9a4eb02
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php
@@ -0,0 +1,30 @@
+get(TemplateResource::class);
+/** @var TemplateInterfaceFactory $templateFactory */
+$templateFactory = $objectManager->get(TemplateInterfaceFactory::class);
+/** @var TemplateInterface $template */
+$template = $templateFactory->create();
+
+$content = <<{{trans "Customer create account email no password template"}}
+{{template config_path="design/email/footer_template"}}
+HTML;
+
+$template->setTemplateCode('customer_create_account_email_no_password_template')
+ ->setTemplateText($content)
+ ->setTemplateType(TemplateInterface::TYPE_HTML);
+$templateResource->save($template);
diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_rollback.php
new file mode 100644
index 0000000000000..4e14b4293cbb5
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_rollback.php
@@ -0,0 +1,25 @@
+get(TemplateResource::class);
+/** @var Collection $collection */
+$collection = $objectManager->get(CollectionFactory::class)->create();
+/** @var TemplateInterface $template */
+$template = $collection
+ ->addFieldToFilter('template_code', 'customer_create_account_email_no_password_template')
+ ->getFirstItem();
+if ($template->getId()) {
+ $templateResource->delete($template);
+}
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php
new file mode 100644
index 0000000000000..ca1f309c5cc9b
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php
@@ -0,0 +1,108 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Remember::class)
+ ->setTemplate('Magento_Persistent::remember_me.phtml');
+ }
+
+ /**
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/remember_enabled 1
+ * @magentoConfigFixture current_store persistent/options/remember_default 0
+ *
+ * @return void
+ */
+ public function testRememberMeEnabled(): void
+ {
+ $this->assertFalse($this->block->isRememberMeChecked());
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf(
+ '//input[@name="persistent_remember_me"]/following-sibling::label/span[contains(text(), "%s")]',
+ __('Remember Me')
+ ),
+ $this->block->toHtml()
+ ),
+ 'Remember Me checkbox wasn\'t found.'
+ );
+ }
+
+ /**
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/remember_enabled 1
+ * @magentoConfigFixture current_store persistent/options/remember_default 1
+ *
+ * @return void
+ */
+ public function testRememberMeAndRememberDefaultEnabled(): void
+ {
+ $this->assertTrue($this->block->isRememberMeChecked());
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf(
+ '//input[@name="persistent_remember_me"]/following-sibling::label/span[contains(text(), "%s")]',
+ __('Remember Me')
+ ),
+ $this->block->toHtml()
+ ),
+ 'Remember Me checkbox wasn\'t found or not checked by default.'
+ );
+ }
+
+ /**
+ * @magentoConfigFixture current_store persistent/options/enabled 0
+ *
+ * @return void
+ */
+ public function testPersistentDisabled(): void
+ {
+ $this->assertEmpty($this->block->toHtml());
+ }
+
+ /**
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/remember_enabled 0
+ *
+ * @return void
+ */
+ public function testRememberMeDisabled(): void
+ {
+ $this->assertEmpty($this->block->toHtml());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php
new file mode 100644
index 0000000000000..16ce015d89ecd
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php
@@ -0,0 +1,80 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->helper = $this->objectManager->get(Session::class);
+ $this->sessionFactory = $this->objectManager->get(SessionFactory::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Persistent/_files/persistent.php
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ *
+ * @return void
+ */
+ public function testPersistentEnabled(): void
+ {
+ $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1));
+ $this->assertTrue($this->helper->isPersistent());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Persistent/_files/persistent.php
+ * @magentoConfigFixture current_store persistent/options/enabled 0
+ *
+ * @return void
+ */
+ public function testPersistentDisabled(): void
+ {
+ $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1));
+ $this->assertFalse($this->helper->isPersistent());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Customer/_files/customer.php
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ *
+ * @return void
+ */
+ public function testCustomerWithoutPersistent(): void
+ {
+ $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1));
+ $this->assertFalse($this->helper->isPersistent());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php
new file mode 100644
index 0000000000000..803e1502e3ad9
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php
@@ -0,0 +1,160 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->configProvider = $this->objectManager->get(DefaultConfigProvider::class);
+ $this->customerSession = $this->objectManager->get(CustomerSession::class);
+ $this->checkoutSession = $this->objectManager->get(CheckoutSession::class);
+ $this->quoteIdMask = $this->objectManager->get(QuoteIdMaskFactory::class)->create();
+ $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class);
+ $this->persistentSession = $this->objectManager->get(PersistentSessionFactory::class)->create();
+ $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->customerSession->setCustomerId(null);
+ $this->checkoutSession->clearQuote();
+ $this->checkoutSession->setCustomerData(null);
+ $this->persistentSessionHelper->setSession(null);
+
+ parent::tearDown();
+ }
+
+ /**
+ * @return void
+ */
+ public function testPluginIsRegistered(): void
+ {
+ $pluginInfo = $this->objectManager->get(PluginList::class)->get(DefaultConfigProvider::class);
+ $this->assertSame(ConfigProviderPlugin::class, $pluginInfo['mask_quote_id_substitutor']['instance']);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ *
+ * @return void
+ */
+ public function testWithNotLoggedCustomer(): void
+ {
+ $session = $this->persistentSession->loadByCustomerId(1);
+ $this->persistentSessionHelper->setSession($session);
+ $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $this->checkoutSession->setQuoteId($quote->getId());
+ $result = $this->configProvider->getConfig();
+ $this->assertEquals(
+ $this->quoteIdMask->load($quote->getId(), 'quote_id')->getMaskedId(),
+ $result['quoteData']['entity_id']
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ *
+ * @return void
+ */
+ public function testWithLoggedCustomer(): void
+ {
+ $this->customerSession->setCustomerId(1);
+ $session = $this->persistentSession->loadByCustomerId(1);
+ $this->persistentSessionHelper->setSession($session);
+ $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $this->checkoutSession->setQuoteId($quote->getId());
+ $result = $this->configProvider->getConfig();
+ $this->assertEquals($quote->getId(), $result['quoteData']['entity_id']);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php
+ * @magentoConfigFixture current_store persistent/options/enabled 0
+ *
+ * @return void
+ */
+ public function testPersistentDisabled(): void
+ {
+ $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $this->checkoutSession->setQuoteId($quote->getId());
+ $result = $this->configProvider->getConfig();
+ $this->assertNull($result['quoteData']['entity_id']);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ *
+ * @return void
+ */
+ public function testWithoutPersistentSession(): void
+ {
+ $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $this->checkoutSession->setQuoteId($quote->getId());
+ $result = $this->configProvider->getConfig();
+ $this->assertNull($result['quoteData']['entity_id']);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php
new file mode 100644
index 0000000000000..176224bad7a1f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php
@@ -0,0 +1,84 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->model = $this->objectManager->get(CheckoutConfigProvider::class);
+ }
+
+ /**
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/remember_enabled 1
+ * @magentoConfigFixture current_store persistent/options/remember_default 1
+ *
+ * @return void
+ */
+ public function testRememberMeEnabled(): void
+ {
+ $expectedConfig = [
+ 'persistenceConfig' => ['isRememberMeCheckboxVisible' => true, 'isRememberMeCheckboxChecked' => true],
+ ];
+ $config = $this->model->getConfig();
+ $this->assertEquals($expectedConfig, $config);
+ }
+
+ /**
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/remember_enabled 0
+ * @magentoConfigFixture current_store persistent/options/remember_default 0
+ *
+ * @return void
+ */
+ public function testRememberMeDisabled(): void
+ {
+ $expectedConfig = [
+ 'persistenceConfig' => ['isRememberMeCheckboxVisible' => false, 'isRememberMeCheckboxChecked' => false],
+ ];
+ $config = $this->model->getConfig();
+ $this->assertEquals($expectedConfig, $config);
+ }
+
+ /**
+ * @magentoConfigFixture current_store persistent/options/enabled 0
+ * @magentoConfigFixture current_store persistent/options/remember_default 0
+ *
+ * @return void
+ */
+ public function testPersistentDisabled(): void
+ {
+ $expectedConfig = [
+ 'persistenceConfig' => ['isRememberMeCheckboxVisible' => false, 'isRememberMeCheckboxChecked' => false],
+ ];
+ $config = $this->model->getConfig();
+ $this->assertEquals($expectedConfig, $config);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php
new file mode 100644
index 0000000000000..e11d47af3e814
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php
@@ -0,0 +1,116 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->model = $this->objectManager->get(QuoteManager::class);
+ $this->checkoutSession = $this->objectManager->get(CheckoutSession::class);
+ $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class);
+ $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class);
+ $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->checkoutSession->clearQuote();
+ $this->checkoutSession->setCustomerData(null);
+ if ($this->quote instanceof CartInterface) {
+ $this->quoteRepository->delete($this->quote);
+ }
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/shopping_cart 1
+ *
+ * @return void
+ */
+ public function testPersistentShoppingCartEnabled(): void
+ {
+ $customerQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $this->checkoutSession->setQuoteId($customerQuote->getId());
+ $this->model->setGuest(true);
+ $this->quote = $this->checkoutSession->getQuote();
+ $this->assertNotEquals($customerQuote->getId(), $this->quote->getId());
+ $this->assertFalse($this->model->isPersistent());
+ $this->assertNull($this->quote->getCustomerId());
+ $this->assertNull($this->quote->getCustomerEmail());
+ $this->assertNull($this->quote->getCustomerFirstname());
+ $this->assertNull($this->quote->getCustomerLastname());
+ $this->assertEquals(GroupInterface::NOT_LOGGED_IN_ID, $this->quote->getCustomerGroupId());
+ $this->assertEmpty($this->quote->getIsPersistent());
+ $this->assertNull($this->persistentSessionHelper->getSession()->getId());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/shopping_cart 0
+ *
+ * @return void
+ */
+ public function testPersistentShoppingCartDisabled(): void
+ {
+ $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $this->checkoutSession->setQuoteId($quote->getId());
+ $this->model->setGuest(true);
+ $this->assertNull($this->checkoutSession->getQuote()->getId());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php
index 35f2283494b1c..bd4d24211f1e3 100644
--- a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php
+++ b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php
@@ -10,16 +10,21 @@
use DateTime;
use DateTimeZone;
use Magento\Customer\Api\CustomerRepositoryInterface;
-use Magento\Customer\Api\Data\CustomerInterface;
-use Magento\Framework\Event;
-use Magento\Framework\Event\Observer;
+use Magento\Customer\Model\Session as CustomerSession;
use Magento\Framework\ObjectManagerInterface;
+use Magento\Framework\Stdlib\CookieManagerInterface;
+use Magento\Persistent\Helper\Session as PersistentSessionHelper;
use Magento\Persistent\Model\Session;
use Magento\Persistent\Model\SessionFactory;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
/**
+ * Test for synchronize persistent session on login observer
+ *
+ * @see \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver
+ * @magentoAppArea frontend
+ * @magentoDbIsolation enabled
* @magentoDataFixture Magento/Customer/_files/customer.php
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
@@ -28,87 +33,129 @@ class SynchronizePersistentOnLoginObserverTest extends TestCase
/**
* @var SynchronizePersistentOnLoginObserver
*/
- protected $_model;
+ private $model;
/**
* @var ObjectManagerInterface
*/
- protected $_objectManager;
+ private $objectManager;
/**
- * @var \Magento\Persistent\Helper\Session
+ * @var PersistentSessionHelper
*/
- protected $_persistentSession;
+ private $persistentSessionHelper;
/**
- * @var \Magento\Customer\Model\Session
+ * @var CustomerRepositoryInterface
*/
- protected $_customerSession;
+ private $customerRepository;
/**
- * @var CustomerInterface
+ * @var SessionFactory
*/
- private $customer;
+ private $persistentSessionFactory;
+
+ /**
+ * @var CookieManagerInterface
+ */
+ private $cookieManager;
+
+ /**
+ * @var CustomerSession
+ */
+ private $customerSession;
/**
* @inheritDoc
*/
protected function setUp(): void
{
- $this->_objectManager = Bootstrap::getObjectManager();
- $this->_persistentSession = $this->_objectManager->get(\Magento\Persistent\Helper\Session::class);
- $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class);
- $this->_model = $this->_objectManager->create(
- SynchronizePersistentOnLoginObserver::class,
- [
- 'persistentSession' => $this->_persistentSession,
- 'customerSession' => $this->_customerSession
- ]
- );
- /** @var CustomerRepositoryInterface $customerRepository */
- $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class);
- $this->customer = $customerRepository->getById(1);
+ parent::setUp();
+
+ $this->objectManager = Bootstrap::getObjectManager();
+ $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class);
+ $this->model = $this->objectManager->get(SynchronizePersistentOnLoginObserver::class);
+ $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class);
+ $this->persistentSessionFactory = $this->objectManager->get(SessionFactory::class);
+ $this->cookieManager = $this->objectManager->get(CookieManagerInterface::class);
+ $this->customerSession = $this->objectManager->get(CustomerSession::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->persistentSessionHelper->setRememberMeChecked(null);
+ $this->customerSession->logout();
+
+ parent::tearDown();
}
/**
* Test that persistent session is created on customer login
+ *
+ * @return void
*/
public function testSynchronizePersistentOnLogin(): void
{
- $sessionModel = $this->_objectManager->create(Session::class);
- $sessionModel->loadByCustomerId($this->customer->getId());
+ $customer = $this->customerRepository->get('customer@example.com');
+ $sessionModel = $this->persistentSessionFactory->create();
+ $sessionModel->loadByCustomerId($customer->getId());
$this->assertNull($sessionModel->getCustomerId());
- $event = new Event();
- $observer = new Observer(['event' => $event]);
- $event->setData('customer', $this->customer);
- $this->_persistentSession->setRememberMeChecked(true);
- $this->_model->execute($observer);
- // check that persistent session has been stored for Customer
- /** @var Session $sessionModel */
- $sessionModel = $this->_objectManager->create(Session::class);
- $sessionModel->loadByCustomerId($this->customer->getId());
- $this->assertEquals($this->customer->getId(), $sessionModel->getCustomerId());
+ $this->persistentSessionHelper->setRememberMeChecked(true);
+ $this->customerSession->loginById($customer->getId());
+ $sessionModel = $this->persistentSessionFactory->create();
+ $sessionModel->loadByCustomerId($customer->getId());
+ $this->assertEquals($customer->getId(), $sessionModel->getCustomerId());
}
/**
* Test that expired persistent session is renewed on customer login
+ *
+ * @return void
*/
public function testExpiredPersistentSessionShouldBeRenewedOnLogin(): void
{
+ $customer = $this->customerRepository->get('customer@example.com');
$lastUpdatedAt = (new DateTime('-1day'))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s');
- /** @var Session $sessionModel */
- $sessionModel = $this->_objectManager->create(SessionFactory::class)->create();
- $sessionModel->setCustomerId($this->customer->getId());
+ $sessionModel = $this->persistentSessionFactory->create();
+ $sessionModel->setCustomerId($customer->getId());
$sessionModel->setUpdatedAt($lastUpdatedAt);
$sessionModel->save();
- $event = new Event();
- $observer = new Observer(['event' => $event]);
- $event->setData('customer', $this->customer);
- $this->_persistentSession->setRememberMeChecked(true);
- $this->_model->execute($observer);
- /** @var Session $sessionModel */
- $sessionModel = $this->_objectManager->create(Session::class);
- $sessionModel->loadByCustomerId(1);
+ $this->persistentSessionHelper->setRememberMeChecked(true);
+ $this->customerSession->loginById($customer->getId());
+ $sessionModel = $this->persistentSessionFactory->create();
+ $sessionModel->loadByCustomerId($customer->getId());
$this->assertGreaterThan($lastUpdatedAt, $sessionModel->getUpdatedAt());
}
+
+ /**
+ * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php
+ * @magentoConfigFixture current_store persistent/options/enabled 0
+ *
+ * @return void
+ */
+ public function testDisabledPersistentSession(): void
+ {
+ $customer = $this->customerRepository->get('customer@example.com');
+ $this->customerSession->loginById($customer->getId());
+ $this->assertNull($this->cookieManager->getCookie(Session::COOKIE_NAME));
+ }
+
+ /**
+ * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/lifetime 0
+ *
+ * @return void
+ */
+ public function testDisabledPersistentSessionLifetime(): void
+ {
+ $customer = $this->customerRepository->get('customer@example.com');
+ $this->customerSession->loginById($customer->getId());
+ $session = $this->persistentSessionFactory->create()->setLoadExpired()->loadByCustomerId($customer->getId());
+ $this->assertNull($session->getId());
+ $this->assertNull($this->cookieManager->getCookie(Session::COOKIE_NAME));
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php
index 2bf97fdb4953f..293f1d1890d92 100644
--- a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php
+++ b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php
@@ -3,54 +3,77 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Persistent\Observer;
+use Magento\Customer\Model\Session as CustomerSession;
+use Magento\Framework\ObjectManagerInterface;
+use Magento\Persistent\Model\SessionFactory;
+use Magento\TestFramework\Helper\Bootstrap;
+use PHPUnit\Framework\TestCase;
+
/**
+ * Test for synchronize persistent on logout observer
+ *
+ * @see \Magento\Persistent\Observer\SynchronizePersistentOnLogoutObserver
* @magentoDataFixture Magento/Customer/_files/customer.php
+ * @magentoAppArea frontend
+ * @magentoDbIsolation enabled
*/
-class SynchronizePersistentOnLogoutObserverTest extends \PHPUnit\Framework\TestCase
+class SynchronizePersistentOnLogoutObserverTest extends TestCase
{
- /**
- * @var \Magento\Framework\ObjectManagerInterface
- */
- protected $_objectManager;
+ /** @var ObjectManagerInterface */
+ private $objectManager;
+
+ /** @var CustomerSession */
+ private $customerSession;
+
+ /** @var SessionFactory */
+ private $sessionFactory;
/**
- * @var \Magento\Customer\Model\Session
+ * @inheritdoc
*/
- protected $_customerSession;
-
protected function setUp(): void
{
- $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
- $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class);
+ parent::setUp();
+
+ $this->objectManager = Bootstrap::getObjectManager();
+ $this->customerSession = $this->objectManager->get(CustomerSession::class);
+ $this->sessionFactory = $this->objectManager->get(SessionFactory::class);
}
/**
* @magentoConfigFixture current_store persistent/options/enabled 1
* @magentoConfigFixture current_store persistent/options/logout_clear 1
- * @magentoAppArea frontend
- * @magentoAppIsolation enabled
+ *
+ * @return void
*/
- public function testSynchronizePersistentOnLogout()
+ public function testSynchronizePersistentOnLogout(): void
{
- $this->_customerSession->loginById(1);
-
- // check that persistent session has been stored for Customer
- /** @var \Magento\Persistent\Model\Session $sessionModel */
- $sessionModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
- \Magento\Persistent\Model\Session::class
- );
+ $this->customerSession->loginById(1);
+ $sessionModel = $this->sessionFactory->create();
$sessionModel->loadByCookieKey();
$this->assertEquals(1, $sessionModel->getCustomerId());
-
- $this->_customerSession->logout();
-
- /** @var \Magento\Persistent\Model\Session $sessionModel */
- $sessionModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
- \Magento\Persistent\Model\Session::class
- );
+ $this->customerSession->logout();
+ $sessionModel = $this->sessionFactory->create();
$sessionModel->loadByCookieKey();
$this->assertNull($sessionModel->getCustomerId());
}
+
+ /**
+ * @magentoConfigFixture current_store persistent/options/enabled 1
+ * @magentoConfigFixture current_store persistent/options/logout_clear 0
+ *
+ * @return void
+ */
+ public function testSynchronizePersistentOnLogoutDisabled(): void
+ {
+ $this->customerSession->loginById(1);
+ $this->customerSession->logout();
+ $sessionModel = $this->sessionFactory->create();
+ $sessionModel->loadByCookieKey();
+ $this->assertEquals(1, $sessionModel->getCustomerId());
+ }
}
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_rollback.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_rollback.php
new file mode 100644
index 0000000000000..581ddb35e3678
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_rollback.php
@@ -0,0 +1,11 @@
+requireDataFixture('Magento/Customer/_files/customer_address_rollback.php');
+Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php');
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php
new file mode 100644
index 0000000000000..a2c68ad9b7f2a
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php
@@ -0,0 +1,19 @@
+requireDataFixture('Magento/Checkout/_files/quote_with_customer_without_address.php');
+
+$objectManager = Bootstrap::getObjectManager();
+/** @var SessionFactory $persistentSessionFactory */
+$persistentSessionFactory = $objectManager->get(SessionFactory::class);
+$session = $persistentSessionFactory->create();
+$session->setCustomerId(1)->save();
+$session->setPersistentCookie(10000, '');
diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php
new file mode 100644
index 0000000000000..252b3f4be7079
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php
@@ -0,0 +1,17 @@
+get(SessionFactory::class);
+$sessionFactory->create()->deleteByCustomerId(1);
+
+Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_customer_without_address_rollback.php');
diff --git a/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php
index f16986a3f2422..391be01b17f45 100644
--- a/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php
+++ b/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php
@@ -133,6 +133,6 @@ public function testChangeQuoteCustomerGroupIdForCustomerWithEnabledAutomaticGro
);
$this->model->execute($eventObserver);
- $this->assertEquals(1, $quote->getCustomer()->getGroupId());
+ $this->assertEquals(2, $quote->getCustomer()->getGroupId());
}
}
diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php
new file mode 100644
index 0000000000000..df5f5f8336303
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php
@@ -0,0 +1,80 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->page = $this->objectManager->get(PageFactory::class)->create();
+ }
+
+ /**
+ * @return void
+ */
+ public function testMyProductReviewsLink(): void
+ {
+ $this->preparePage();
+ $block = $this->page->getLayout()->getBlock('customer-account-navigation-product-reviews-link');
+ $this->assertNotFalse($block);
+ $html = $block->toHtml();
+ $this->assertStringContainsString('/review/customer/', $html);
+ $this->assertEquals((string)__('My Product Reviews'), strip_tags($html));
+ }
+
+ /**
+ * @magentoConfigFixture current_store catalog/review/active 0
+ *
+ * @return void
+ */
+ public function testMyProductReviewsLinkDisabled(): void
+ {
+ $this->preparePage();
+ $block = $this->page->getLayout()->getBlock('customer-account-navigation-product-reviews-link');
+ $this->assertFalse($block);
+ }
+
+ /**
+ * Prepare page before render
+ *
+ * @return void
+ */
+ private function preparePage(): void
+ {
+ $this->page->addHandle([
+ 'default',
+ 'customer_account',
+ ]);
+ $this->page->getLayout()->generateXml();
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php
new file mode 100644
index 0000000000000..24cb2fe76a6d4
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php
@@ -0,0 +1,135 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->customerSession = $this->objectManager->get(Session::class);
+ $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(ListCustomer::class)
+ ->setTemplate('Magento_Review::customer/list.phtml');
+ $this->collectionFactory = $this->objectManager->get(CollectionFactory::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->customerSession->setCustomerId(null);
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Review/_files/customer_review_with_rating.php
+ *
+ * @return void
+ */
+ public function testCustomerProductReviewsGrid(): void
+ {
+ $this->customerSession->setCustomerId(1);
+ $review = $this->collectionFactory->create()->addCustomerFilter(1)->addReviewSummary()->getFirstItem();
+ $this->assertNotNull($review->getReviewId());
+ $blockHtml = $this->block->toHtml();
+ $createdDate = $this->block->dateFormat($review->getReviewCreatedAt());
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf("//td[contains(@class, 'date') and contains(text(), '%s')]", $createdDate),
+ $blockHtml
+ ),
+ sprintf('Created date wasn\'t found or not equals to %s.', $createdDate)
+ );
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf("//td[contains(@class, 'item')]//a[contains(text(), '%s')]", $review->getName()),
+ $blockHtml
+ ),
+ 'Product name wasn\'t found.'
+ );
+ $rating = $review->getSum() / $review->getCount();
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf("//td[contains(@class, 'summary')]//span[contains(text(), '%s%%')]", $rating),
+ $blockHtml
+ ),
+ sprintf('Rating wasn\'t found or not equals to %s%%.', $rating)
+ );
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf("//td[contains(@class, 'description') and contains(text(), '%s')]", $review->getDetail()),
+ $blockHtml
+ ),
+ 'Review description wasn\'t found.'
+ );
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf(
+ "//td[contains(@class, 'actions')]//a[contains(@href, '%s')]/span[contains(text(), '%s')]",
+ $this->block->getReviewUrl($review),
+ __('See Details')
+ ),
+ $blockHtml
+ ),
+ sprintf('%s button wasn\'t found.', __('See Details'))
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/Customer/_files/customer.php
+ *
+ * @return void
+ */
+ public function testCustomerWithoutReviews(): void
+ {
+ $this->customerSession->setCustomerId(1);
+ $this->assertStringContainsString(
+ (string)__('You have submitted no reviews.'),
+ strip_tags($this->block->toHtml())
+ );
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php
new file mode 100644
index 0000000000000..31a342ad8ac54
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php
@@ -0,0 +1,138 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->customerSession = $this->objectManager->get(Session::class);
+ $this->collectionFactory = $this->objectManager->get(CollectionFactory::class);
+ $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(View::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->customerSession->setCustomerId(null);
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Review/_files/customer_review_with_rating.php
+ *
+ * @return void
+ */
+ public function testCustomerProductReviewBlock(): void
+ {
+ $this->customerSession->setCustomerId(1);
+ $review = $this->collectionFactory->create()->addCustomerFilter(1)->getFirstItem();
+ $this->assertNotNull($review->getReviewId());
+ $blockHtml = $this->block->setReviewId($review->getReviewId())->toHtml();
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf("//div[contains(@class, 'product-info')]/h2[contains(text(), '%s')]", $review->getName()),
+ $blockHtml
+ ),
+ 'Product name wasn\'t found.'
+ );
+ $ratings = $this->block->getRating();
+ $this->assertCount(2, $ratings);
+ foreach ($ratings as $rating) {
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf(
+ "//div[contains(@class, 'rating-summary')]//span[contains(text(), '%s')]"
+ . "/../..//span[contains(text(), '%s%%')]",
+ $rating->getRatingCode(),
+ $rating->getPercent()
+ ),
+ $blockHtml
+ ),
+ sprintf('Rating %s was not found or not equals to %s.', $rating->getRatingCode(), $rating->getPercent())
+ );
+ }
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf("//div[contains(@class, 'review-title') and contains(text(), '%s')]", $review->getTitle()),
+ $blockHtml
+ ),
+ 'Review title wasn\'t found.'
+ );
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf("//div[contains(@class, 'review-content') and contains(text(), '%s')]", $review->getDetail()),
+ $blockHtml
+ ),
+ 'Review description wasn\'t found.'
+ );
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf(
+ "//div[contains(@class, 'review-date') and contains(text(), '%s')]/time[contains(text(), '%s')]",
+ __('Submitted on'),
+ $this->block->dateFormat($review->getCreatedAt())
+ ),
+ $blockHtml
+ ),
+ 'Created date wasn\'t found.'
+ );
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ sprintf(
+ "//a[contains(@href, '/review/customer/')]/span[contains(text(), '%s')]",
+ __('Back to My Reviews')
+ ),
+ $blockHtml
+ ),
+ sprintf('%s button wasn\'t found.', __('Back to My Reviews'))
+ );
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php
new file mode 100644
index 0000000000000..b26b71803848f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php
@@ -0,0 +1,69 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class);
+ $this->session = $this->objectManager->get(Quote::class);
+ $this->layout = $this->objectManager->get(LayoutInterface::class);
+ $this->block = $this->layout->createBlock(Grid::class);
+ $this->layout->createBlock(Items::class)->setChild('items_grid', $this->block);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php
+ *
+ * @return void
+ */
+ public function testGetItems(): void
+ {
+ $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $this->session->setQuoteId($quote->getId());
+ $items = $this->block->getItems();
+ $this->assertCount(1, $items);
+ $this->assertEquals('simple2', reset($items)->getSku());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php
new file mode 100644
index 0000000000000..291fda6e2494f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php
@@ -0,0 +1,80 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Cart::class);
+ $this->session = $this->objectManager->get(Quote::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->session->clearStorage();
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php
+ *
+ * @return void
+ */
+ public function testGetItemCollection(): void
+ {
+ $this->session->setCustomerId(1);
+ $items = $this->block->getItemCollection();
+ $this->assertCount(1, $items);
+ $this->assertEquals('simple2', reset($items)->getSku());
+ }
+
+ /**
+ * @return void
+ */
+ public function testClearShoppingCartButton(): void
+ {
+ $confirmation = __('Are you sure you want to delete all items from shopping cart?');
+ $button = $this->block->getChildBlock('empty_customer_cart_button');
+ $this->assertEquals(sprintf("order.clearShoppingCart('%s')", $confirmation), $button->getOnclick());
+ $this->assertEquals(__('Clear Shopping Cart'), $button->getLabel());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php
new file mode 100644
index 0000000000000..1b5772cec66de
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php
@@ -0,0 +1,86 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Items::class);
+ $this->orderFactory = $this->objectManager->get(OrderFactory::class);
+ $this->registry = $this->objectManager->get(Registry::class);
+ $this->invoiceCollectionFactory = $this->objectManager->get(CollectionFactory::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->registry->unregister('current_invoice');
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/invoice.php
+ *
+ * @return void
+ */
+ public function testGetUpdateButtonHtml(): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId('100000001');
+ $invoice = $this->invoiceCollectionFactory->create()->setOrderFilter($order)->setPageSize(1)->getFirstItem();
+ $this->registry->unregister('current_invoice');
+ $this->registry->register('current_invoice', $invoice);
+ $this->block->toHtml();
+ $button = $this->block->getChildBlock('update_button');
+ $this->assertEquals((string)__('Update Qty\'s'), (string)$button->getLabel());
+ $this->assertStringContainsString(
+ sprintf('sales/index/updateQty/order_id/%u/', (int)$order->getEntityId()),
+ $button->getOnClick()
+ );
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php
new file mode 100644
index 0000000000000..a78c221cb5f84
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php
@@ -0,0 +1,115 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->objectManager->addSharedInstance(
+ $this->objectManager->get(AuthorizationMock::class),
+ Authorization::class
+ );
+ $this->registry = $this->objectManager->get(Registry::class);
+ $this->orderFactory = $this->objectManager->get(OrderFactory::class);
+ $this->layout = $this->objectManager->get(LayoutInterface::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->registry->unregister('sales_order');
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @return void
+ */
+ public function testInvoiceButton(): void
+ {
+ $this->registerOrder('100000001');
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath(
+ '//button[@id=\'order_invoice\']',
+ $this->layout->createBlock(View::class)->getButtonsHtml()
+ )
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php
+ *
+ * @return void
+ */
+ public function testInvoiceButtonIsNotVisible(): void
+ {
+ $this->registerOrder('100000001');
+ $this->assertEmpty(
+ Xpath::getElementsCountForXpath(
+ '//button[@id=\'order_invoice\']',
+ $this->layout->createBlock(View::class)->getButtonsHtml()
+ )
+ );
+ }
+
+ /**
+ * Register order
+ *
+ * @param OrderInterface $order
+ * @return void
+ */
+ private function registerOrder(string $orderIncrementId): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId);
+ $this->registry->unregister('sales_order');
+ $this->registry->register('sales_order', $order);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php
new file mode 100644
index 0000000000000..b6aa44bac1c4d
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php
@@ -0,0 +1,301 @@
+layout = $this->_objectManager->get(LayoutInterface::class);
+ $this->getQuoteByReservedOrderId = $this->_objectManager->get(GetQuoteByReservedOrderId::class);
+ $this->session = $this->_objectManager->get(Quote::class);
+ $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class);
+ $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->quoteIdsToRemove[] = $this->session->getQuote()->getId();
+ foreach ($this->quoteIdsToRemove as $quoteId) {
+ try {
+ $this->quoteRepository->delete($this->quoteRepository->get($quoteId));
+ } catch (NoSuchEntityException $e) {
+ //do nothing
+ }
+ }
+
+ $this->session->clearStorage();
+
+ parent::tearDown();
+ }
+
+ /**
+ * @dataProvider responseFlagsProvider
+ *
+ * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php
+ *
+ * @param bool $asJson
+ * @param bool $asJsVarname
+ * @return void
+ */
+ public function testAddProductToOrderFromShoppingCart(bool $asJson, bool $asJsVarname): void
+ {
+ $oldQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $params = $this->hydrateParams([
+ 'json' => $asJson,
+ 'as_js_varname' => $asJsVarname,
+ ]);
+ $post = $this->hydratePost([
+ 'sidebar' => [
+ 'add_cart_item' => [
+ $oldQuote->getItemsCollection()->getFirstItem()->getId() => 1,
+ ],
+ ],
+ ]);
+
+ $this->dispatchWitParams($params, $post);
+
+ $this->checkHandles(explode(',', $params['block']), $asJson);
+ $this->checkQuotes($oldQuote, 'simple2');
+
+ if ($asJsVarname) {
+ $this->assertRedirect($this->stringContains('sales/order_create/showUpdateResult'));
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function responseFlagsProvider(): array
+ {
+ return [
+ 'as_json' => [
+ 'as_json' => true,
+ 'as_js_varname' => false,
+ ],
+ 'as_plain' => [
+ 'as_json' => false,
+ 'as_js_varname' => true,
+ ],
+ ];
+ }
+
+ /**
+ * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php
+ *
+ * @return void
+ */
+ public function testRemoveProductFromShoppingCart(): void
+ {
+ $oldQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $post = $this->hydratePost([
+ 'sidebar' => [
+ 'remove' => [
+ $oldQuote->getItemsCollection()->getFirstItem()->getId() => 'cart',
+ ],
+ ],
+ ]);
+ $params = $this->hydrateParams();
+
+ $this->dispatchWitParams($params, $post);
+
+ $this->checkHandles(explode(',', $params['block']));
+ $this->checkQuotes($oldQuote);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php
+ *
+ * @return void
+ */
+ public function testClearShoppingCart(): void
+ {
+ $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address');
+ $post = $this->hydratePost([
+ 'sidebar' => [
+ 'empty_customer_cart' => '1',
+ ],
+ ]);
+ $params = $this->hydrateParams();
+
+ $this->dispatchWitParams($params, $post);
+
+ $this->checkHandles(explode(',', $params['block']));
+ $this->assertEmpty($quote->getItemsCollection(false)->getItems());
+ }
+
+ /**
+ * @magentoDataFixture Magento/Checkout/_files/inactive_quote_with_customer.php
+ *
+ * @return void
+ */
+ public function testMoveFromOrderToShoppingCart(): void
+ {
+ $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_inactive_quote');
+ $this->session->setQuoteId($quote->getId());
+ $post = $this->hydratePost([
+ 'update_items' => '1',
+ 'item' => [
+ $quote->getItemsCollection()->getFirstItem()->getId() => [
+ 'qty' => '1',
+ 'use_discount' => '1',
+ 'action' => 'cart',
+ ],
+ ],
+ ]);
+ $params = $this->hydrateParams(['blocks' => null]);
+ $this->dispatchWitParams($params, $post);
+ $customerCart = $this->quoteRepository->getForCustomer(1);
+ $cartItems = $customerCart->getItemsCollection();
+ $this->assertCount(1, $cartItems->getItems());
+ $this->assertEquals('taxable_product', $cartItems->getFirstItem()->getSku());
+ $this->quoteIdsToRemove[] = $customerCart->getId();
+ }
+
+ /**
+ * Check customer quotes
+ *
+ * @param CartInterface $oldQuote
+ * @param string|null $expectedSku
+ * @return void
+ */
+ private function checkQuotes(CartInterface $oldQuote, ?string $expectedSku = null): void
+ {
+ $newQuote = $this->session->getQuote();
+ $oldQuoteItemCollection = $oldQuote->getItemsCollection(false);
+ $this->assertEmpty($oldQuoteItemCollection->getItems());
+ $newQuoteItemsCollection = $newQuote->getItemsCollection(false);
+
+ if ($expectedSku !== null) {
+ $this->assertNotNull($newQuoteItemsCollection->getItemByColumnValue('sku', $expectedSku));
+ } else {
+ $this->assertEmpty($newQuoteItemsCollection->getItems());
+ }
+ }
+
+ /**
+ * Check that all required handles were applied
+ *
+ * @param array $blocks
+ * @param bool $asJson
+ * @return void
+ */
+ private function checkHandles(array $blocks, bool $asJson = true): void
+ {
+ $handles = $this->layout->getUpdate()->getHandles();
+
+ if ($asJson) {
+ $this->assertContains('sales_order_create_load_block_message', $handles);
+ $this->assertContains('sales_order_create_load_block_json', $handles);
+ } else {
+ $this->assertContains('sales_order_create_load_block_plain', $handles);
+ }
+
+ foreach ($blocks as $block) {
+ $this->assertContains(
+ 'sales_order_create_load_block_' . $block,
+ $handles
+ );
+ }
+ }
+
+ /**
+ * Fill post params array to proper state
+ *
+ * @param array $inputArray
+ * @return array
+ */
+ private function hydratePost(array $inputArray = []): array
+ {
+ return array_merge(
+ [
+ 'customer_id' => 1,
+ 'store_id' => $this->storeManager->getStore('default')->getId(),
+ 'sidebar' => [],
+ ],
+ $inputArray
+ );
+ }
+
+ /**
+ * Fill params array to proper state
+ *
+ * @param array $inputArray
+ * @return array
+ */
+ private function hydrateParams(array $inputArray = []): array
+ {
+ return array_merge(
+ [
+ 'json' => true,
+ 'block' => 'sidebar,items,shipping_method,billing_method,totals,giftmessage',
+ 'as_js_varname' => true,
+ ],
+ $inputArray
+ );
+ }
+
+ /**
+ * Dispatch request with params
+ *
+ * @param array $params
+ * @param array $postParams
+ * @return void
+ */
+ private function dispatchWitParams(array $params, array $postParams): void
+ {
+ $this->getRequest()->setMethod(Http::METHOD_POST)
+ ->setPostValue($postParams)
+ ->setParams($params);
+ $this->dispatch('backend/sales/order_create/loadBlock');
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php
index 726ba697beb12..3c26a53424d81 100644
--- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AbstractInvoiceControllerTest.php
@@ -7,39 +7,34 @@
namespace Magento\Sales\Controller\Adminhtml\Order\Invoice;
-use Magento\Framework\Api\SearchCriteria;
use Magento\Framework\Api\SearchCriteriaBuilder;
-use Magento\Framework\Data\Form\FormKey;
+use Magento\Framework\App\Request\Http;
use Magento\Sales\Api\Data\InvoiceInterface;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Model\OrderRepository;
+use Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory;
use Magento\TestFramework\Mail\Template\TransportBuilderMock;
use Magento\TestFramework\TestCase\AbstractBackendController;
/**
* Abstract backend invoice test.
*/
-class AbstractInvoiceControllerTest extends AbstractBackendController
+abstract class AbstractInvoiceControllerTest extends AbstractBackendController
{
- /**
- * @var TransportBuilderMock
- */
+ /** @var TransportBuilderMock */
protected $transportBuilder;
- /**
- * @var OrderRepository
- */
- protected $orderRepository;
+ /** @var string */
+ protected $resource = 'Magento_Sales::sales_invoice';
- /**
- * @var FormKey
- */
- protected $formKey;
+ /** @var OrderRepository */
+ private $orderRepository;
- /**
- * @var string
- */
- protected $resource = 'Magento_Sales::sales_invoice';
+ /** @var SearchCriteriaBuilder */
+ private $searchCriteriaBuilder;
+
+ /** @var CollectionFactory */
+ private $invoiceCollectionFactory;
/**
* @inheritdoc
@@ -47,46 +42,71 @@ class AbstractInvoiceControllerTest extends AbstractBackendController
protected function setUp(): void
{
parent::setUp();
+
$this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class);
$this->orderRepository = $this->_objectManager->get(OrderRepository::class);
- $this->formKey = $this->_objectManager->get(FormKey::class);
+ $this->searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class);
+ $this->invoiceCollectionFactory = $this->_objectManager->get(CollectionFactory::class);
}
/**
+ * Retrieve order
+ *
* @param string $incrementalId
* @return OrderInterface|null
*/
- protected function getOrder(string $incrementalId)
+ protected function getOrder(string $incrementalId): ?OrderInterface
{
- /** @var SearchCriteria $searchCriteria */
- $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class)
- ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId)
+ $searchCriteria = $this->searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementalId)
->create();
-
$orders = $this->orderRepository->getList($searchCriteria)->getItems();
- /** @var OrderInterface $order */
- $order = reset($orders);
- return $order;
+ return reset($orders);
}
/**
- * @param OrderInterface $order
+ * Get firs order invoice
+ *
+ * @param OrderInterface|int $order
* @return InvoiceInterface
*/
- protected function getInvoiceByOrder(OrderInterface $order): InvoiceInterface
+ protected function getInvoiceByOrder($order): InvoiceInterface
{
- /** @var \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection $invoiceCollection */
- $invoiceCollection = $this->_objectManager->create(
- \Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory::class
- )->create();
+ $invoiceCollection = $this->invoiceCollectionFactory->create();
- /** @var InvoiceInterface $invoice */
- $invoice = $invoiceCollection
- ->setOrderFilter($order)
- ->setPageSize(1)
- ->getFirstItem();
+ return $invoiceCollection->setOrderFilter($order)->setPageSize(1)->getFirstItem();
+ }
- return $invoice;
+ /**
+ * Prepare request
+ *
+ * @param array $postParams
+ * @param array $params
+ * @return void
+ */
+ protected function prepareRequest(array $postParams = [], array $params = []): void
+ {
+ $this->getRequest()->setMethod(Http::METHOD_POST);
+ $this->getRequest()->setParams($params);
+ $this->getRequest()->setPostValue($postParams);
+ }
+
+ /**
+ * Normalize post parameters
+ *
+ * @param array $items
+ * @param string $commentText
+ * @param bool $doShipment
+ * @return array
+ */
+ protected function hydratePost(array $items, string $commentText = '', $doShipment = false): array
+ {
+ return [
+ 'invoice' => [
+ 'items' => $items,
+ 'comment_text' => $commentText,
+ 'do_shipment' => $doShipment
+ ],
+ ];
}
}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php
index ee59a55acd9b1..c7711e8897696 100644
--- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php
@@ -30,10 +30,11 @@ class AddCommentTest extends AbstractInvoiceControllerTest
public function testSendEmailOnAddInvoiceComment(): void
{
$comment = 'Test Invoice Comment';
- $order = $this->prepareRequest(
- [
- 'comment' => ['comment' => $comment, 'is_customer_notified' => true],
- ]
+ $order = $this->getOrder('100000001');
+ $invoice = $this->getInvoiceByOrder($order);
+ $this->prepareRequest(
+ ['comment' => ['comment' => $comment, 'is_customer_notified' => true]],
+ ['id' => $invoice->getEntityId()]
);
$this->dispatch('backend/sales/order_invoice/addComment');
@@ -41,6 +42,7 @@ public function testSendEmailOnAddInvoiceComment(): void
$this->assertStringContainsString($comment, $html);
$message = $this->transportBuilder->getSentMessage();
+ $this->assertNotNull($message);
$subject = __('Update to your %1 invoice', $order->getStore()->getFrontendName())->render();
$messageConstraint = $this->logicalAnd(
new StringContains($order->getCustomerName()),
@@ -55,7 +57,8 @@ public function testSendEmailOnAddInvoiceComment(): void
);
$this->assertEquals($message->getSubject(), $subject);
- $this->assertThat($message->getBody()->getParts()[0]->getRawContent(), $messageConstraint);
+ $bodyParts = $message->getBody()->getParts();
+ $this->assertThat(reset($bodyParts)->getRawContent(), $messageConstraint);
}
/**
@@ -63,7 +66,7 @@ public function testSendEmailOnAddInvoiceComment(): void
*/
public function testAclHasAccess()
{
- $this->prepareRequest(['comment' => ['comment' => 'Comment']]);
+ $this->prepareRequest();
parent::testAclHasAccess();
}
@@ -73,31 +76,8 @@ public function testAclHasAccess()
*/
public function testAclNoAccess()
{
- $this->prepareRequest(['comment' => ['comment' => 'Comment']]);
+ $this->prepareRequest();
parent::testAclNoAccess();
}
-
- /**
- * @param array $params
- * @return \Magento\Sales\Api\Data\OrderInterface|null
- */
- private function prepareRequest(array $params = [])
- {
- $order = $this->getOrder('100000001');
- $invoice = $this->getInvoiceByOrder($order);
-
- $this->getRequest()->setMethod('POST');
- $this->getRequest()->setParams(
- [
- 'id' => $invoice->getEntityId(),
- 'form_key' => $this->formKey->getFormKey(),
- ]
- );
-
- $data = $params ?? [];
- $this->getRequest()->setPostValue($data);
-
- return $order;
- }
}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewActionTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewActionTest.php
new file mode 100644
index 0000000000000..c8444c827d2e4
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewActionTest.php
@@ -0,0 +1,78 @@
+orderFactory = $this->_objectManager->get(OrderFactory::class);
+ $this->escaper = $this->_objectManager->get(Escaper::class);
+ }
+
+ /**
+ * @return void
+ */
+ public function testWithNoExistingOrder(): void
+ {
+ $this->dispatchWithOrderId(863521);
+ $expectedMessage = (string)__("The entity that was requested doesn't exist. Verify the entity and try again.");
+ $this->assertSessionMessages($this->containsEqual($this->escaper->escapeHtml($expectedMessage)));
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php
+ *
+ * @return void
+ */
+ public function testCanNotInvoice(): void
+ {
+ $expectedMessage = __('The order does not allow an invoice to be created.');
+ $order = $this->orderFactory->create()->loadByIncrementId('100000001');
+ $this->dispatchWithOrderId((int)$order->getEntityId());
+ $this->assertSessionMessages($this->containsEqual((string)$expectedMessage), MessageInterface::TYPE_ERROR);
+ }
+
+ /**
+ * Dispatch request with order_id param
+ *
+ * @param int $orderId
+ * @return void
+ */
+ private function dispatchWithOrderId(int $orderId): void
+ {
+ $this->getRequest()->setMethod(Http::METHOD_GET)
+ ->setParams(['order_id' => $orderId]);
+ $this->dispatch('backend/sales/order_invoice/new');
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php
index a09e656b90726..13003e40dc0a3 100644
--- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/SaveTest.php
@@ -7,38 +7,53 @@
namespace Magento\Sales\Controller\Adminhtml\Order\Invoice;
+use Magento\Framework\Escaper;
+use Magento\Sales\Api\Data\InvoiceInterface;
+use Magento\Sales\Model\Order;
use PHPUnit\Framework\Constraint\StringContains;
/**
- * Class tests invoice creation in backend.
+ * Class tests invoice creation in admin panel.
+ *
+ * @see \Magento\Sales\Controller\Adminhtml\Order\Invoice\Save
*
* @magentoDbIsolation enabled
* @magentoAppArea adminhtml
- * @magentoDataFixture Magento/Sales/_files/order.php
*/
class SaveTest extends AbstractInvoiceControllerTest
{
+ /** @var string */
+ protected $uri = 'backend/sales/order_invoice/save';
+
+ /** @var Escaper */
+ private $escaper;
+
/**
- * @var string
+ * @inheritdoc
*/
- protected $uri = 'backend/sales/order_invoice/save';
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->escaper = $this->_objectManager->get(Escaper::class);
+ }
/**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
* @return void
*/
public function testSendEmailOnInvoiceSave(): void
{
- $order = $this->prepareRequest();
+ $order = $this->getOrder('100000001');
+ $itemId = $order->getItemsCollection()->getFirstItem()->getId();
+ $post = $this->hydratePost([$itemId => 2]);
+ $this->prepareRequest($post, ['order_id' => $order->getEntityId()]);
$this->dispatch('backend/sales/order_invoice/save');
-
- $this->assertSessionMessages(
- $this->equalTo([(string)__('The invoice has been created.')]),
- \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS
- );
- $this->assertRedirect($this->stringContains('sales/order/view/order_id/' . $order->getEntityId()));
-
$invoice = $this->getInvoiceByOrder($order);
+ $this->checkSuccess($invoice, 2);
$message = $this->transportBuilder->getSentMessage();
+ $this->assertNotNull($message);
$subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render();
$messageConstraint = $this->logicalAnd(
new StringContains($invoice->getBillingAddress()->getName()),
@@ -49,9 +64,113 @@ public function testSendEmailOnInvoiceSave(): void
"Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}"
)
);
-
$this->assertEquals($message->getSubject(), $subject);
- $this->assertThat($message->getBody()->getParts()[0]->getRawContent(), $messageConstraint);
+ $bodyParts = $message->getBody()->getParts();
+ $this->assertThat(reset($bodyParts)->getRawContent(), $messageConstraint);
+ }
+
+ /**
+ * @magentoConfigFixture current_store sales_email/invoice/enabled 0
+ *
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @return void
+ */
+ public function testSendEmailOnInvoiceSaveWithDisabledConfig(): void
+ {
+ $order = $this->getOrder('100000001');
+ $post = $this->hydratePost([$order->getItemsCollection()->getFirstItem()->getId() => 2]);
+ $this->prepareRequest($post, ['order_id' => $order->getEntityId()]);
+ $this->dispatch('backend/sales/order_invoice/save');
+ $this->checkSuccess($this->getInvoiceByOrder($order), 2);
+ $this->assertNull($this->transportBuilder->getSentMessage());
+ }
+
+ /**
+ * @dataProvider invoiceDataProvider
+ *
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @param int $invoicedItemsQty
+ * @param string $commentMessage
+ * @param bool $doShipment
+ * @return void
+ */
+ public function testSuccessfulInvoice(
+ int $invoicedItemsQty,
+ string $commentMessage = '',
+ bool $doShipment = false
+ ): void {
+ $order = $this->getOrder('100000001');
+ $post = $this->hydratePost(
+ [$order->getItemsCollection()->getFirstItem()->getId() => $invoicedItemsQty],
+ $commentMessage,
+ $doShipment
+ );
+ $this->prepareRequest($post, ['order_id' => $order->getEntityId()]);
+ $this->dispatch('backend/sales/order_invoice/save');
+ $this->checkSuccess($this->getInvoiceByOrder($order), $invoicedItemsQty, $commentMessage, $doShipment);
+ }
+
+ /**
+ * @return array
+ */
+ public function invoiceDataProvider(): array
+ {
+ return [
+ 'with_comment_message' => [
+ 'invoiced_items_qty' => 2,
+ 'comment_message' => 'test comment message',
+ ],
+ 'partial_invoice' => [
+ 'invoiced_items_qty' => 1,
+ ],
+ 'with_do_shipment' => [
+ 'invoiced_items_qty' => 2,
+ 'comment_message' => '',
+ 'do_shipment' => true,
+ ],
+ ];
+ }
+
+ /**
+ * @return void
+ */
+ public function testWitNoExistingOrder(): void
+ {
+ $expectedMessage = (string)__('The order no longer exists.');
+ $this->prepareRequest(['order_id' => 899989]);
+ $this->dispatch('backend/sales/order_invoice/save');
+ $this->assertErrorResponse($expectedMessage);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php
+ *
+ * @return void
+ */
+ public function testCanNotInvoiceOrder(): void
+ {
+ $expectedMessage = (string)__('The order does not allow an invoice to be created.');
+ $order = $this->getOrder('100000001');
+ $this->prepareRequest([], ['order_id' => $order->getEntityId()]);
+ $this->dispatch('backend/sales/order_invoice/save');
+ $this->assertErrorResponse($expectedMessage);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @return void
+ */
+ public function testInvoiceWithoutQty(): void
+ {
+ $expectedMessage = (string)__('The invoice can\'t be created without products. Add products and try again.');
+ $order = $this->getOrder('100000001');
+ $post = $this->hydratePost([$order->getItemsCollection()->getFirstItem()->getId() => '0']);
+ $this->prepareRequest($post, ['order_id' => $order->getEntityId()]);
+ $this->dispatch('backend/sales/order_invoice/save');
+ $this->assertErrorResponse($this->escaper->escapeHtml($expectedMessage));
}
/**
@@ -77,11 +196,14 @@ public function testAclNoAccess()
/**
* Checks that order protect code is not changing after invoice submitting
*
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
* @return void
*/
public function testOrderProtectCodePreserveAfterInvoiceSave(): void
{
- $order = $this->prepareRequest();
+ $order = $this->getOrder('100000001');
+ $this->prepareRequest([], ['order_id' => $order->getEntityId()]);
$protectCode = $order->getProtectCode();
$this->dispatch($this->uri);
$invoicedOrder = $this->getOrder('100000001');
@@ -90,23 +212,46 @@ public function testOrderProtectCodePreserveAfterInvoiceSave(): void
}
/**
- * @param array $params
- * @return \Magento\Sales\Api\Data\OrderInterface|null
+ * Check error response
+ *
+ * @param string $expectedMessage
+ * @return void
*/
- private function prepareRequest(array $params = [])
+ private function assertErrorResponse(string $expectedMessage): void
{
- $order = $this->getOrder('100000001');
- $this->getRequest()->setMethod('POST');
- $this->getRequest()->setParams(
- [
- 'order_id' => $order->getEntityId(),
- 'form_key' => $this->formKey->getFormKey(),
- ]
- );
+ $this->assertRedirect($this->stringContains('sales/order_invoice/new'));
+ $this->assertSessionMessages($this->containsEqual($expectedMessage));
+ }
- $data = $params ?? [];
- $this->getRequest()->setPostValue($data);
+ /**
+ * Check that invoice was successfully created
+ *
+ * @param InvoiceInterface $invoice
+ * @param int $invoicedItemsQty
+ * @param string|null $commentMessage
+ * @param bool $doShipment
+ * @return void
+ */
+ private function checkSuccess(
+ InvoiceInterface $invoice,
+ int $invoicedItemsQty,
+ ?string $commentMessage = null,
+ bool $doShipment = false
+ ): void {
+ $message = $doShipment ? 'You created the invoice and shipment.' : 'The invoice has been created.';
+ $expectedState = $doShipment ? Order::STATE_COMPLETE : Order::STATE_PROCESSING;
+ $this->assertNotNull($invoice->getEntityId());
+ $this->assertEquals($invoicedItemsQty, (int)$invoice->getTotalQty());
+ $order = $invoice->getOrder();
+ $this->assertEquals($expectedState, $order->getState());
- return $order;
+ if ($commentMessage) {
+ $this->assertEquals($commentMessage, $invoice->getCustomerNote());
+ }
+
+ $this->assertRedirect(
+ $this->stringContains(sprintf('sales/order/view/order_id/%u', (int)$order->getEntityId()))
+ );
+ $this->assertSessionMessages($this->containsEqual((string)__($message)));
}
}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php
new file mode 100644
index 0000000000000..5eb554ef937d5
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php
@@ -0,0 +1,66 @@
+orderFactory = $this->_objectManager->get(OrderFactory::class);
+ $this->session = $this->_objectManager->get(Session::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->session->getInvoiceItemQtys(true);
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @return void
+ */
+ public function testExecute(): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId('100000001');
+ $this->session->setInvoiceItemQtys('test');
+ $this->getRequest()->setMethod(Http::METHOD_GET)->setParams(['order_id' => $order->getEntityId()]);
+ $this->dispatch('backend/sales/order_invoice/start');
+ $this->assertRedirect($this->stringContains('sales/order_invoice/new'));
+ $this->assertNull($this->session->getInvoiceItemQtys());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php
new file mode 100644
index 0000000000000..2b91c5d04fd6f
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php
@@ -0,0 +1,123 @@
+json = $this->_objectManager->get(SerializerInterface::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @return void
+ */
+ public function testSuccess(): void
+ {
+ $order = $this->getOrder('100000001');
+ $itemId = $order->getItemsCollection()->getFirstItem()->getId();
+ $qtyToInvoice = 1;
+ $invoicedItemsXpath = sprintf(
+ "//input[contains(@class, 'qty-input') and @name='invoice[items][%u]' and @value='%u']",
+ $itemId,
+ $qtyToInvoice
+ );
+ $post = $this->hydratePost([$itemId => $qtyToInvoice]);
+ $this->prepareRequest($post, ['order_id' => $order->getEntityId()]);
+ $this->dispatch('backend/sales/order_invoice/updateQty');
+ $this->assertEquals(
+ 1,
+ Xpath::getElementsCountForXpath($invoicedItemsXpath, $this->getResponse()->getContent())
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php
+ *
+ * @return void
+ */
+ public function testCanNotInvoice(): void
+ {
+ $order = $this->getOrder('100000001');
+ $itemId = $order->getItemsCollection()->getFirstItem()->getId();
+ $post = $this->hydratePost([$itemId => '1']);
+ $this->prepareRequest($post, ['order_id' => $order->getEntityId()]);
+ $this->dispatch('backend/sales/order_invoice/updateQty');
+ $this->assertErrorResponse('The order does not allow an invoice to be created.');
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @return void
+ */
+ public function testWithoutQty(): void
+ {
+ $order = $this->getOrder('100000001');
+ $itemId = $order->getItemsCollection()->getFirstItem()->getId();
+ $post = $this->hydratePost([$itemId => '0']);
+ $this->prepareRequest($post, ['order_id' => $order->getEntityId()]);
+ $this->dispatch('backend/sales/order_invoice/updateQty');
+ $this->assertErrorResponse(
+ 'The invoice can\'t be created without products. Add products and try again.'
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testWithNoExistingOrderId(): void
+ {
+ $post = $this->hydratePost([
+ 'invoice' => [
+ 'items' => [
+ '1' => '3',
+ ],
+ ],
+ ]);
+ $this->prepareRequest($post, ['order_id' => 6543265]);
+ $this->dispatch('backend/sales/order_invoice/updateQty');
+ $this->assertErrorResponse('The order no longer exists.');
+ }
+
+ /**
+ * Check error response
+ *
+ * @param string $expectedMessage
+ * @return void
+ */
+ private function assertErrorResponse(string $expectedMessage): void
+ {
+ $expectedResponse = [
+ 'error' => true,
+ 'message' => (string)__($expectedMessage),
+ ];
+ $response = $this->getResponse()->getContent();
+ $this->assertNotEmpty($response);
+ $this->assertEquals($expectedResponse, $this->json->unserialize($response));
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php
new file mode 100644
index 0000000000000..cffdda80cc897
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php
@@ -0,0 +1,139 @@
+checkoutSession = $this->_objectManager->get(CheckoutSession::class);
+ $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class);
+ $this->cookieManager = $this->_objectManager->get(CookieManagerInterface::class);
+ $this->customerSession = $this->_objectManager->get(Session::class);
+ $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $createdQuoteId = $this->checkoutSession->getQuoteId();
+
+ if ($createdQuoteId !== null) {
+ try {
+ $this->quoteRepository->delete($this->quoteRepository->get($createdQuoteId));
+ } catch (NoSuchEntityException $e) {
+ //already deleted
+ }
+ }
+
+ $this->customerSession->setCustomerId(null);
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDbIsolation disabled
+ *
+ * @magentoDataFixture Magento/Sales/_files/order_by_guest_with_simple_product.php
+ *
+ * @return void
+ */
+ public function testReorderSimpleProduct(): void
+ {
+ $orderIncrementId = 'test_order_1';
+ $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId);
+ $cookieValue = base64_encode($order->getProtectCode() . ':' . $orderIncrementId);
+ $this->cookieManager->setPublicCookie(Guest::COOKIE_NAME, $cookieValue);
+ $this->dispatchReorderRequest();
+ $this->assertRedirect($this->stringContains('checkout/cart'));
+ $quoteId = $this->checkoutSession->getQuoteId();
+ $this->assertNotNull($quoteId);
+ $quoteItemsCollection = $this->quoteRepository->get((int)$quoteId)->getItemsCollection();
+ $this->assertCount(1, $quoteItemsCollection);
+ $this->assertEquals(
+ $order->getItemsCollection()->getFirstItem()->getSku(),
+ $quoteItemsCollection->getFirstItem()->getSku()
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testReorderWithoutParamsAndCookie(): void
+ {
+ $this->dispatchReorderRequest();
+ $this->assertRedirect($this->stringContains('sales/guest/form'));
+ $this->assertSessionMessages(
+ $this->containsEqual((string)__('You entered incorrect data. Please try again.')),
+ MessageInterface::TYPE_ERROR
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/Customer/_files/customer.php
+ *
+ * @return void
+ */
+ public function testReorderGuestOrderByCustomer(): void
+ {
+ $this->customerSession->setCustomerId(1);
+ $this->dispatchReorderRequest();
+ $this->assertRedirect($this->stringContains('sales/order/history'));
+ }
+
+ /**
+ * Dispatch reorder request.
+ *
+ * @return void
+ */
+ private function dispatchReorderRequest(): void
+ {
+ $this->getRequest()->setMethod(Request::METHOD_POST);
+ $this->dispatch('sales/guest/reorder/');
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php
new file mode 100644
index 0000000000000..3b32e7238cc76
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php
@@ -0,0 +1,151 @@
+checkoutSession = $this->_objectManager->get(CheckoutSession::class);
+ $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class);
+ $this->customerSession = $this->_objectManager->get(Session::class);
+ $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class);
+ $this->escaper = $this->_objectManager->get(Escaper::class);
+ $this->versionChecker = $this->_objectManager->get(View::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ if ($this->quote instanceof CartInterface) {
+ $this->quoteRepository->delete($this->quote);
+ }
+ $this->customerSession->setCustomerId(null);
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/customer_order_with_taxable_product.php
+ *
+ * @return void
+ */
+ public function testReorder(): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId('test_order_with_taxable_product');
+ $this->customerSession->setCustomerId($order->getCustomerId());
+ $this->dispatchReorderRequest((int)$order->getId());
+ $this->assertRedirect($this->stringContains('checkout/cart'));
+ $this->quote = $this->checkoutSession->getQuote();
+ $quoteItemsCollection = $this->quote->getItemsCollection();
+ $this->assertCount(1, $quoteItemsCollection);
+ $this->assertEquals(
+ $order->getItemsCollection()->getFirstItem()->getSku(),
+ $quoteItemsCollection->getFirstItem()->getSku()
+ );
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/customer_order_with_simple_product.php
+ *
+ * @return void
+ */
+ public function testReorderProductLowQty(): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId('55555555');
+ $this->customerSession->setCustomerId($order->getCustomerId());
+ $this->dispatchReorderRequest((int)$order->getId());
+ $origMessage = (string)__('The requested qty is not available');
+ $message = $this->escaper->escapeHtml(
+ __('Could not add the product with SKU "%1" to the shopping cart: %2', 'simple-1', $origMessage)
+ );
+ $constraint = $this->logicalOr($this->containsEqual($origMessage), $this->containsEqual($message));
+ $this->assertThat($this->getMessages(MessageInterface::TYPE_ERROR), $constraint);
+ $this->quote = $this->checkoutSession->getQuote();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Customer/_files/customer.php
+ * @magentoDataFixture Magento/Sales/_files/customer_order_with_two_items.php
+ *
+ * @return void
+ */
+ public function testReorderByAnotherCustomer(): void
+ {
+ $this->customerSession->setCustomerId(1);
+ $order = $this->orderFactory->create()->loadByIncrementId('100000555');
+ $this->dispatchReorderRequest((int)$order->getId());
+
+ if ($this->versionChecker->isVersionUpdated()) {
+ $this->assertRedirect($this->stringContains('noroute'));
+ } else {
+ $this->assertRedirect($this->stringContains('sales/order/history'));
+ }
+ }
+
+ /**
+ * Dispatch reorder request.
+ *
+ * @param null|int $orderId
+ * @return void
+ */
+ private function dispatchReorderRequest(?int $orderId = null): void
+ {
+ $this->getRequest()->setMethod(Request::METHOD_POST);
+ $this->getRequest()->setParam('order_id', $orderId);
+ $this->dispatch('sales/order/reorder/');
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php
new file mode 100644
index 0000000000000..5a21f551ff1a7
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php
@@ -0,0 +1,106 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->helper = $this->objectManager->get(Reorder::class);
+ $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class);
+ $this->customerSession = $this->objectManager->get(Session::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function tearDown(): void
+ {
+ $this->customerSession->setCustomerId(null);
+
+ parent::tearDown();
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @return void
+ */
+ public function testCanReorderForGuest(): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId('100000001');
+ $this->assertTrue($this->helper->canReorder($order->getId()));
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/customer_order_with_two_items.php
+ *
+ * @return void
+ */
+ public function testCanReorderForLoggedCustomer(): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId('100000555');
+ $this->customerSession->setCustomerId($order->getCustomerId());
+ $this->assertTrue($this->helper->canReorder($order->getId()));
+ }
+
+ /**
+ * @magentoDataFixture Magento/Customer/_files/customer.php
+ * @magentoDataFixture Magento/Sales/_files/order_state_hold.php
+ *
+ * @return void
+ */
+ public function testCanReorderHoldOrderForLoggedCustomer(): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId('100000001');
+ $this->customerSession->setCustomerId(1);
+ $this->assertFalse($this->helper->canReorder($order->getId()));
+ }
+
+ /**
+ * @magentoConfigFixture current_store sales/reorder/allow 0
+ * @magentoDataFixture Magento/Sales/_files/order.php
+ *
+ * @return void
+ */
+ public function testCanReorderConfigDisabled(): void
+ {
+ $order = $this->orderFactory->create()->loadByIncrementId('100000001');
+ $this->assertFalse($this->helper->canReorder($order->getId()));
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.php
new file mode 100644
index 0000000000000..ca102b0fabf89
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.php
@@ -0,0 +1,22 @@
+requireDataFixture('Magento/Checkout/_files/customer_quote_ready_for_order.php');
+
+$objectManager = Bootstrap::getObjectManager();
+/** @var CartRepositoryInterface $quoteRepository */
+$quoteRepository = $objectManager->get(CartRepositoryInterface::class);
+/** @var CartManagementInterface $quoteManagement */
+$quoteManagement = $objectManager->get(CartManagementInterface::class);
+
+$quote = $quoteRepository->getActiveForCustomer(1);
+$quoteManagement->placeOrder($quote->getId());
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_rollback.php
new file mode 100644
index 0000000000000..46cabc2e3fd9b
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_rollback.php
@@ -0,0 +1,33 @@
+get(Registry::class);
+/** @var OrderRepositoryInterface $orderRepository */
+$orderRepository = $objectManager->get(OrderRepositoryInterface::class);
+/** @var OrderInterfaceFactory $orderFactory */
+$orderFactory = $objectManager->get(OrderInterfaceFactory::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+$order = $orderFactory->create()->loadByIncrementId('55555555');
+if ($order->getId()) {
+ $orderRepository->delete($order);
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
+
+Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php');
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php
new file mode 100644
index 0000000000000..59ec4182ac870
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php
@@ -0,0 +1,30 @@
+requireDataFixture('Magento/Checkout/_files/quote_with_taxable_product_and_customer.php');
+
+$objectManager = Bootstrap::getObjectManager();
+/** @var CartRepositoryInterface $quoteRepository */
+$quoteRepository = $objectManager->get(CartRepositoryInterface::class);
+/** @var CartManagementInterface $quoteManagement */
+$quoteManagement = $objectManager->get(CartManagementInterface::class);
+/** @var PaymentInterface $payment */
+$payment = $objectManager->get(PaymentInterface::class);
+$payment->setMethod('checkmo');
+
+$quote = $quoteRepository->getActiveForCustomer(1);
+$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate');
+$quote->getShippingAddress()->setCollectShippingRates(true);
+$quote->getShippingAddress()->collectShippingRates();
+$quoteRepository->save($quote);
+$quoteManagement->placeOrder($quote->getId(), $payment);
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product_rollback.php
new file mode 100644
index 0000000000000..d42f6a1140286
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product_rollback.php
@@ -0,0 +1,35 @@
+get(Registry::class);
+/** @var OrderRepositoryInterface $orderRepository */
+$orderRepository = $objectManager->get(OrderRepositoryInterface::class);
+/** @var OrderInterfaceFactory $orderFactory */
+$orderFactory = $objectManager->get(OrderInterfaceFactory::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+$order = $orderFactory->create()->loadByIncrementId('test_order_with_taxable_product');
+if ($order->getId()) {
+ $orderRepository->delete($order);
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
+
+Resolver::getInstance()->requireDataFixture(
+ 'Magento/Checkout/_files/quote_with_taxable_product_and_customer_rollback.php'
+);
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php
new file mode 100644
index 0000000000000..c3bab9acca27b
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php
@@ -0,0 +1,31 @@
+requireDataFixture('Magento/Checkout/_files/quote_with_address_saved.php');
+
+$objectManager = Bootstrap::getObjectManager();
+/** @var CartRepositoryInterface $quoteRepository */
+$quoteRepository = $objectManager->get(CartRepositoryInterface::class);
+/** @var CartManagementInterface $quoteManagement */
+$quoteManagement = $objectManager->get(CartManagementInterface::class);
+/** @var PaymentInterface $payment */
+$payment = $objectManager->get(PaymentInterface::class);
+
+$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('test_order_1');
+$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate');
+$quote->getShippingAddress()->setCollectShippingRates(true);
+$quote->getShippingAddress()->collectShippingRates();
+$quoteRepository->save($quote);
+$payment->setMethod('checkmo');
+$quoteManagement->placeOrder($quote->getId(), $payment);
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_rollback.php
new file mode 100644
index 0000000000000..b4ec514d1311e
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_rollback.php
@@ -0,0 +1,33 @@
+get(Registry::class);
+/** @var OrderRepositoryInterface $orderRepository */
+$orderRepository = $objectManager->get(OrderRepositoryInterface::class);
+/** @var OrderInterfaceFactory $orderFactory */
+$orderFactory = $objectManager->get(OrderInterfaceFactory::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+$order = $orderFactory->create()->loadByIncrementId('test_order_1');
+if ($order->getId()) {
+ $orderRepository->delete($order);
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
+
+Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address_saved_rollback.php');
diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php
new file mode 100644
index 0000000000000..954c23498ec66
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php
@@ -0,0 +1,108 @@
+resourceConnection = Bootstrap::getObjectManager()->get(ResourceConnection::class);
+ $this->initSalesRule();
+ }
+
+ /**
+ * Prepare request
+ *
+ * @return void
+ */
+ private function prepareRequest(): void
+ {
+ $couponList = $this->getCouponsIdList();
+ if (count($couponList)) {
+ $this->getRequest()->setParams(['internal_ids' => $couponList[0]])->setMethod('POST');
+ }
+ }
+
+ /**
+ * Init current sales rule
+ *
+ * @return void
+ */
+ private function initSalesRule(): void
+ {
+ /** @var RuleCollection $collection */
+ $collection = Bootstrap::getObjectManager()->create(RuleCollection::class);
+ $collection->addFieldToFilter('name', 'Rule with coupon list');
+ $this->salesRule = $collection->getFirstItem();
+ }
+
+ /**
+ * Retrieve id list of coupons
+ *
+ * @return array
+ */
+ private function getCouponsIdList(): array
+ {
+ $select = $this->resourceConnection->getConnection()
+ ->select()
+ ->from($this->resourceConnection->getTableName('salesrule_coupon'))
+ ->columns(['coupon_id'])
+ ->where('rule_id=?', $this->salesRule->getId());
+
+ return $this->resourceConnection->getConnection()->fetchCol($select);
+ }
+
+ /**
+ * Test export csv
+ *
+ * @return void
+ */
+ public function testExportCsv(): void
+ {
+ $this->prepareRequest();
+ $this->dispatch($this->uri);
+ $this->assertStringNotContainsString('404 Error', $this->getResponse()->getBody());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php
new file mode 100644
index 0000000000000..d222b064a0d2c
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php
@@ -0,0 +1,108 @@
+resourceConnection = Bootstrap::getObjectManager()->get(ResourceConnection::class);
+ $this->initSalesRule();
+ }
+
+ /**
+ * Prepare request
+ *
+ * @return void
+ */
+ private function prepareRequest(): void
+ {
+ $couponList = $this->getCouponsIdList();
+ if (count($couponList)) {
+ $this->getRequest()->setParams(['internal_ids' => $couponList[0]])->setMethod('POST');
+ }
+ }
+
+ /**
+ * Init current sales rule
+ *
+ * @return void
+ */
+ private function initSalesRule(): void
+ {
+ /** @var RuleCollection $collection */
+ $collection = Bootstrap::getObjectManager()->create(RuleCollection::class);
+ $collection->addFieldToFilter('name', 'Rule with coupon list');
+ $this->salesRule = $collection->getFirstItem();
+ }
+
+ /**
+ * Retrieve id list of coupons
+ *
+ * @return array
+ */
+ private function getCouponsIdList(): array
+ {
+ $select = $this->resourceConnection->getConnection()
+ ->select()
+ ->from($this->resourceConnection->getTableName('salesrule_coupon'))
+ ->columns(['coupon_id'])
+ ->where('rule_id=?', $this->salesRule->getId());
+
+ return $this->resourceConnection->getConnection()->fetchCol($select);
+ }
+
+ /**
+ * Test export xml
+ *
+ * @return void
+ */
+ public function testExportCsv(): void
+ {
+ $this->prepareRequest();
+ $this->dispatch($this->uri);
+ $this->assertStringNotContainsString('404 Error', $this->getResponse()->getBody());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php
index 3151a76327397..547ce78500f49 100644
--- a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php
+++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php
@@ -4,24 +4,35 @@
* See COPYING.txt for license details.
*/
declare(strict_types=1);
+
+use Magento\Config\Model\ResourceModel\Config;
+use Magento\Directory\Model\Currency as ModelCurrency;
+use Magento\Directory\Model\ResourceModel\Currency as ResourceCurrency;
+use Magento\Store\Model\ScopeInterface;
+use Magento\Store\Model\Store;
+use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\Workaround\Override\Fixture\Resolver;
-$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
-$store = $objectManager->create(\Magento\Store\Model\Store::class);
+$objectManager = Bootstrap::getObjectManager();
+$store = $objectManager->create(Store::class);
$storeId = $store->load('fixture_second_store', 'code')->getId();
if ($storeId) {
- $configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class);
+ $configResource = $objectManager->get(Config::class);
$configResource->deleteConfig(
- \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_DEFAULT,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORES,
+ ModelCurrency::XML_PATH_CURRENCY_DEFAULT,
+ ScopeInterface::SCOPE_STORES,
$storeId
);
$configResource->deleteConfig(
- \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORES,
+ ModelCurrency::XML_PATH_CURRENCY_ALLOW,
+ ScopeInterface::SCOPE_STORES,
$storeId
);
}
Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store_rollback.php');
+$reflectionClass = new \ReflectionClass(ResourceCurrency::class);
+$staticProperty = $reflectionClass->getProperty('_rateCache');
+$staticProperty->setAccessible(true);
+$staticProperty->setValue(null);
diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php
index 5d1758f578836..dd715ecc93b0d 100644
--- a/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php
+++ b/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php
@@ -116,6 +116,7 @@ public function childProductsDataProvider(): array
],
'expected_data' => [
[
+ 'baseOldPrice' => ['amount' => 150],
'oldPrice' => ['amount' => 150],
'basePrice' => ['amount' => 50],
'finalPrice' => ['amount' => 50],
@@ -123,6 +124,7 @@ public function childProductsDataProvider(): array
'msrpPrice' => ['amount' => null],
],
[
+ 'baseOldPrice' => ['amount' => 150],
'oldPrice' => ['amount' => 150],
'basePrice' => ['amount' => 58.55],
'finalPrice' => ['amount' => 58.55],
@@ -130,6 +132,7 @@ public function childProductsDataProvider(): array
'msrpPrice' => ['amount' => null],
],
[
+ 'baseOldPrice' => ['amount' => 150],
'oldPrice' => ['amount' => 150],
'basePrice' => ['amount' => 75],
'finalPrice' => ['amount' => 75],
diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php
index 9a95ed4fd462d..1cbdf6144d640 100644
--- a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php
+++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php
@@ -62,6 +62,23 @@ public function testLoadedProductAttributes()
$this->assertEquals('Short description', $productOnWishlist->getData('short_description'));
}
+ /**
+ * Tests collection load.
+ * Tests collection load method when product salable filter flag is setted to true
+ * and few products are present.
+ *
+ * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php
+ * @magentoDataFixture Magento/Wishlist/_files/wishlist.php
+ * @magentoDbIsolation disabled
+ */
+ public function testLoadWhenFewProductsPresent()
+ {
+ $this->itemCollection->setSalableFilter(true);
+ $this->itemCollection->addCustomerIdFilter(1);
+ $this->itemCollection->load();
+ $this->assertCount(1, $this->itemCollection->getItems());
+ }
+
/**
* @param array $attributes
*/
diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js
index f46ff6b30abbe..c0ecec40516fa 100644
--- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js
+++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js
@@ -246,6 +246,12 @@ define([
expect(type).toEqual('object');
});
+ it('Must be false if "disabled" is true', function () {
+ obj.listVisible(false);
+ obj.disabled(true);
+ obj.toggleListVisible();
+ expect(obj.listVisible()).toEqual(false);
+ });
it('Must be false if "listVisible" is true', function () {
obj.listVisible(true);
obj.toggleListVisible();
| |