diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index af43562984134..b63141b510b85 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -486,7 +486,8 @@ private function fetchTierPrices(array $productIds): array ) ->where( 'ap.' . $productEntityLinkField . ' IN (?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); if ($priceFromFilter !== null) { diff --git a/app/code/Magento/Backend/App/Area/FrontNameResolver.php b/app/code/Magento/Backend/App/Area/FrontNameResolver.php index 6c586781f2d81..a927f52b59d95 100644 --- a/app/code/Magento/Backend/App/Area/FrontNameResolver.php +++ b/app/code/Magento/Backend/App/Area/FrontNameResolver.php @@ -123,7 +123,13 @@ public function isHostBackend() if ($this->scopeConfig->getValue(self::XML_PATH_USE_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE)) { $backendUrl = $this->scopeConfig->getValue(self::XML_PATH_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE); } else { - $backendUrl = $this->scopeConfig->getValue(Store::XML_PATH_UNSECURE_BASE_URL, ScopeInterface::SCOPE_STORE); + $backendUrl = $this->config->getValue(Store::XML_PATH_UNSECURE_BASE_URL); + if ($backendUrl === null) { + $backendUrl = $this->scopeConfig->getValue( + Store::XML_PATH_UNSECURE_BASE_URL, + ScopeInterface::SCOPE_STORE + ); + } } $host = $this->request->getServer('HTTP_HOST', ''); return stripos($this->getHostWithPort($backendUrl), (string) $host) !== false; diff --git a/app/code/Magento/Backend/Block/Widget/Button.php b/app/code/Magento/Backend/Block/Widget/Button.php index 3b5eca6a61779..cb8269f692f02 100644 --- a/app/code/Magento/Backend/Block/Widget/Button.php +++ b/app/code/Magento/Backend/Block/Widget/Button.php @@ -5,9 +5,9 @@ */ namespace Magento\Backend\Block\Widget; +use Magento\Backend\Block\Template\Context; use Magento\Framework\App\ObjectManager; use Magento\Framework\Math\Random; -use Magento\Backend\Block\Template\Context; use Magento\Framework\View\Helper\SecureHtmlRenderer; /** @@ -125,6 +125,9 @@ protected function _prepareAttributes($title, $classes, $disabled) 'value' => $this->getValue(), 'disabled' => $disabled, ]; + if ($this->hasData('onclick_attribute')) { + $attributes['onclick'] = $this->getData('onclick_attribute'); + } if ($this->hasData('backend_button_widget_hook_id')) { $attributes['backend-button-widget-hook-id'] = $this->getData('backend_button_widget_hook_id'); } diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertLinkActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertLinkActionGroup.xml new file mode 100644 index 0000000000000..6fa63d14b9612 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertLinkActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + Assert text and url of the links. + + + + + + + + + diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml index b2b71c4ad3eca..ab5f18780082b 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml @@ -22,6 +22,8 @@ + + diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml index b0fbdb8b5b596..8b0bf1dc963ff 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml @@ -23,70 +23,91 @@ - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - - + + + + - + + + + diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php index be14a51ffb27b..33667f158b9d9 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php @@ -94,4 +94,16 @@ public function getAttributesHtmlDataProvider() ] ]; } + + /** + * Verifies ability of adding button onclick attribute + * + * @return void + */ + public function testOnClickAttribute(): void + { + $this->_blockMock->setData(['onclick_attribute' => 'value']); + $attributes = $this->_blockMock->getAttributesHtml(); + $this->assertStringContainsString('onclick', $attributes); + } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection.php index ead687faff7bc..45018406277f9 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection.php @@ -145,7 +145,8 @@ public function getParentIdsByChild($childId) ['e.entity_id as parent_product_id'] )->where( $this->getMainTable() . '.product_id IN(?)', - $childId + $childId, + \Zend_Db::INT_TYPE ); return $connection->fetchCol($select); diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml index f73941c375a41..8818fadc1d10c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml @@ -42,10 +42,7 @@ - - - - + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml index 8b50fffec091f..6924e389451cd 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml @@ -36,9 +36,7 @@ - - - + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml index f8f98384ee8da..0474de1144f4e 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml @@ -133,9 +133,7 @@ - - - + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml index 93fac3171e9fb..88f992e698181 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml @@ -41,7 +41,7 @@ - + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml index fe4faed29d144..8b19753067593 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml @@ -48,8 +48,7 @@ - - + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml index 5997cdc14ade8..f7bce778cc0d8 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml @@ -39,8 +39,9 @@ + - + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml index 7049299987dff..d127f18355643 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml @@ -96,7 +96,7 @@ - + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml index 24c481c9ddcb2..927cebdf7e508 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml @@ -168,7 +168,7 @@ - + diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml b/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml index 515c2bc56f067..6ddce634ec957 100644 --- a/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml +++ b/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml @@ -42,7 +42,7 @@ - + @@ -63,9 +63,7 @@ - - - + diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index 57cea59bee207..b06edc43cd71d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -209,7 +209,14 @@ public function getImagesJson() */ private function sortImagesByPosition($images) { - if (is_array($images)) { + $nullPositions = []; + foreach ($images as $index => $image) { + if ($image['position'] === null) { + $nullPositions[] = $image; + unset($images[$index]); + } + } + if (is_array($images) && !empty($images)) { usort( $images, function ($imageA, $imageB) { @@ -217,7 +224,7 @@ function ($imageA, $imageB) { } ); } - return $images; + return array_merge($images, $nullPositions); } /** diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php index 3a9d81eed4221..af921959f8e27 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php @@ -5,6 +5,11 @@ */ namespace Magento\Catalog\Block\Product\View\Options\Type; +use DateTimeZone; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\FilterFactory; +use Magento\Framework\Stdlib\DateTime; + /** * Product options text type block * @@ -27,22 +32,30 @@ class Date extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions */ protected $_catalogProductOptionTypeDate; + /** + * @var FilterFactory + */ + private $filterFactory; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper * @param \Magento\Catalog\Helper\Data $catalogData * @param \Magento\Catalog\Model\Product\Option\Type\Date $catalogProductOptionTypeDate * @param array $data + * @param FilterFactory|null $filterFactory */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Pricing\Helper\Data $pricingHelper, \Magento\Catalog\Helper\Data $catalogData, \Magento\Catalog\Model\Product\Option\Type\Date $catalogProductOptionTypeDate, - array $data = [] + array $data = [], + ?FilterFactory $filterFactory = null ) { $this->_catalogProductOptionTypeDate = $catalogProductOptionTypeDate; parent::__construct($context, $pricingHelper, $catalogData, $data); + $this->filterFactory = $filterFactory ?? ObjectManager::getInstance()->get(FilterFactory::class); } /** @@ -77,14 +90,24 @@ public function getDateHtml() public function getCalendarDateHtml() { $option = $this->getOption(); - $value = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId() . '/date'); + $values = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); $yearStart = $this->_catalogProductOptionTypeDate->getYearStart(); $yearEnd = $this->_catalogProductOptionTypeDate->getYearEnd(); - $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); + $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); /** Escape RTL characters which are present in some locales and corrupt formatting */ $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); + $value = null; + if (is_array($values)) { + $date = $this->getInternalDateString($values); + if ($date !== null) { + $dateFilter = $this->filterFactory->create('date', ['format' => $escapedDateFormat]); + $value = $dateFilter->outputFilter($date); + } elseif (isset($values['date'])) { + $value = $values['date']; + } + } $calendar = $this->getLayout()->createBlock( \Magento\Framework\View\Element\Html\Date::class )->setId( @@ -158,8 +181,8 @@ public function getTimeHtml() * Return drop-down html with range of values * * @param string $name Id/name of html select element - * @param int $from Start position - * @param int $to End position + * @param int $from Start position + * @param int $to End position * @param int|null $value Value selected * @return string Formatted Html */ @@ -209,9 +232,8 @@ protected function _getHtmlSelect($name, $value = null) $select->setExtraParams($extraParams); if ($value === null) { - $value = $this->getProduct()->getPreconfiguredValues()->getData( - 'options/' . $option->getId() . '/' . $name - ); + $values = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + $value = is_array($values) ? $this->parseDate($values, $name) : null; } if ($value !== null) { $select->setValue($value); @@ -233,4 +255,56 @@ protected function _getValueWithLeadingZeros($value) } return $value < 10 ? '0' . $value : $value; } + + /** + * Get internal date format of provided value + * + * @param array $value + * @return string|null + */ + private function getInternalDateString(array $value): ?string + { + $result = null; + if (!empty($value['date']) && !empty($value['date_internal'])) { + $dateTimeZone = new DateTimeZone($this->_localeDate->getConfigTimezone()); + $dateTimeObject = date_create_from_format( + DateTime::DATETIME_PHP_FORMAT, + $value['date_internal'], + $dateTimeZone + ); + if ($dateTimeObject !== false) { + $result = $dateTimeObject->format(DateTime::DATE_PHP_FORMAT); + } + } elseif (!empty($value['day']) && !empty($value['month']) && !empty($value['year'])) { + $dateTimeObject = $this->_localeDate->date(); + $dateTimeObject->setDate((int) $value['year'], (int) $value['month'], (int) $value['day']); + $result = $dateTimeObject->format(DateTime::DATE_PHP_FORMAT); + } + return $result; + } + + /** + * Parse option value and return the requested part + * + * @param array $value + * @param string $part [year, month, day, hour, minute, day_part] + * @return string|null + */ + private function parseDate(array $value, string $part): ?string + { + $result = null; + if (!empty($value['date']) && !empty($value['date_internal'])) { + $formatDate = explode(' ', $value['date_internal']); + $date = explode('-', $formatDate[0]); + $value['year'] = $date[0]; + $value['month'] = $date[1]; + $value['day'] = $date[2]; + } + + if (isset($value[$part])) { + $result = (string) $value[$part]; + } + + return $result; + } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php index f3a4b322e29df..404fd27232b93 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -465,10 +465,11 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) [] )->where( 'e.entity_id IN(?)', - $parentIds + $parentIds, + \Zend_Db::INT_TYPE ); if (!empty($excludeIds)) { - $select->where('child_id NOT IN(?)', $excludeIds); + $select->where('child_id NOT IN(?)', $excludeIds, \Zend_Db::INT_TYPE); } $children = $this->getConnection()->fetchCol($select); @@ -479,7 +480,8 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) $this->getIndexTargetTableByDimension($dimensions) )->where( 'entity_id IN(?)', - $children + $children, + \Zend_Db::INT_TYPE ); $query = $select->insertFromSelect($this->_defaultIndexerResource->getIdxTable(), [], false); $this->getConnection()->query($query); @@ -578,13 +580,14 @@ private function getParentProductsTypes(array $productsIds) ['e.entity_id as parent_id', 'type_id'] )->where( 'l.child_id IN(?)', - $productsIds + $productsIds, + \Zend_Db::INT_TYPE ); $pairs = $this->getConnection()->fetchPairs($select); $byType = []; foreach ($pairs as $productId => $productType) { - $byType[$productType][$productId] = $productId; + $byType[$productType][$productId] = (int)$productId; } return $byType; diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index 5a9d53ce80cf8..7a1bd21d78182 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -267,29 +267,50 @@ protected function processNewAndExistingImages($product, array &$images) { foreach ($images as &$image) { if (empty($image['removed'])) { + $isNew = empty($image['value_id']); $data = $this->processNewImage($product, $image); - if (!$product->isObjectNew()) { - $this->resourceModel->deleteGalleryValueInStore( - $image['value_id'], - $product->getData($this->metadata->getLinkField()), - $product->getStoreId() - ); - } // Add per store labels, position, disabled $data['value_id'] = $image['value_id']; $data['label'] = isset($image['label']) ? $image['label'] : ''; - $data['position'] = isset($image['position']) ? (int)$image['position'] : 0; + $data['position'] = isset($image['position']) && $image['position'] !== '' + ? (int)$image['position'] + : null; $data['disabled'] = isset($image['disabled']) ? (int)$image['disabled'] : 0; $data['store_id'] = (int)$product->getStoreId(); $data[$this->metadata->getLinkField()] = (int)$product->getData($this->metadata->getLinkField()); - $this->resourceModel->insertGalleryValueInStore($data); + $this->saveGalleryStoreValue($product, $data); + if ($isNew && $data['store_id'] !== Store::DEFAULT_STORE_ID) { + $dataForDefaultScope = $data; + $dataForDefaultScope['store_id'] = Store::DEFAULT_STORE_ID; + $dataForDefaultScope['disabled'] = 0; + $dataForDefaultScope['label'] = null; + $this->saveGalleryStoreValue($product, $dataForDefaultScope); + } } } } + /** + * Save media gallery store value + * + * @param Product $product + * @param array $data + */ + private function saveGalleryStoreValue(Product $product, array $data): void + { + if (!$product->isObjectNew()) { + $this->resourceModel->deleteGalleryValueInStore( + $data['value_id'], + $data[$this->metadata->getLinkField()], + $data['store_id'] + ); + } + $this->resourceModel->insertGalleryValueInStore($data); + } + /** * Processes image as new. * diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php index a3726207b3024..ed2e09249e495 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php @@ -64,9 +64,9 @@ public function execute($entity, $arguments = []) $this->addMediaDataToProduct( $entity, - $mediaEntries + $this->sortMediaEntriesByPosition($mediaEntries) ); - + return $entity; } @@ -108,7 +108,7 @@ public function getAttribute() * Find default value * * @param string $key - * @param string[] &$image + * @param string[] $image * @return string * @deprecated 101.0.1 * @since 101.0.0 @@ -121,4 +121,30 @@ protected function findDefaultValue($key, &$image) return ''; } + + /** + * Sort media entries by position + * + * @param array $mediaEntries + * @return array + */ + private function sortMediaEntriesByPosition(array $mediaEntries): array + { + $mediaEntriesWithNullPositions = []; + foreach ($mediaEntries as $index => $mediaEntry) { + if ($mediaEntry['position'] === null) { + $mediaEntriesWithNullPositions[] = $mediaEntry; + unset($mediaEntries[$index]); + } + } + if (!empty($mediaEntries)) { + usort( + $mediaEntries, + function ($entryA, $entryB) { + return ($entryA['position'] < $entryB['position']) ? -1 : 1; + } + ); + } + return array_merge($mediaEntries, $mediaEntriesWithNullPositions); + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php index 77ef8ef4853e1..c85c2e47548a0 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php @@ -6,10 +6,11 @@ namespace Magento\Catalog\Model\Product\Option\Type; +use Magento\Catalog\Model\Product\Exception as ProductException; +use Magento\Catalog\Helper\Product as ProductHelper; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Exception\LocalizedException; -use Magento\Catalog\Model\Product\Exception as ProductException; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\App\ObjectManager; @@ -91,6 +92,11 @@ class File extends \Magento\Catalog\Model\Product\Option\Type\DefaultType */ private $filesystem; + /** + * @var ProductHelper + */ + private $productHelper; + /** * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -103,6 +109,7 @@ class File extends \Magento\Catalog\Model\Product\Option\Type\DefaultType * @param array $data * @param Filesystem $filesystem * @param Json|null $serializer + * @param ProductHelper|null $productHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -116,7 +123,8 @@ public function __construct( \Magento\Framework\Escaper $escaper, array $data = [], Filesystem $filesystem = null, - Json $serializer = null + Json $serializer = null, + ProductHelper $productHelper = null ) { $this->_itemOptionFactory = $itemOptionFactory; $this->_urlBuilder = $urlBuilder; @@ -129,6 +137,7 @@ public function __construct( $this->validatorInfo = $validatorInfo; $this->validatorFile = $validatorFile; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $this->productHelper = $productHelper ?: ObjectManager::getInstance()->get(ProductHelper::class); parent::__construct($checkoutSession, $scopeConfig, $data); } @@ -223,12 +232,21 @@ public function validateUserValue($values) $this->setIsValid(true); $option = $this->getOption(); + if (isset($values['files_prefix'])) { + $processingParams = ['files_prefix' => $values['files_prefix']]; + $processingParams = array_merge($this->_getProcessingParams()->getData(), $processingParams); + $this->productHelper->addParamsToBuyRequest($this->getRequest(), $processingParams); + } + /* * Check whether we receive uploaded file or restore file by: reorder/edit configuration or * previous configuration with no newly uploaded file */ $fileInfo = null; - if (isset($values[$option->getId()]) && is_array($values[$option->getId()])) { + if (isset($values[$option->getId()])) { + if (is_string($values[$option->getId()])) { + $values[$option->getId()] = explode(',', $values[$option->getId()]); + } // Legacy style, file info comes in array with option id index $fileInfo = $values[$option->getId()]; } else { diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index 74a6c7f634f81..206f3dba0ee60 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -379,7 +379,7 @@ public function getTierPrices($product) if (array_key_exists('website_price', $price)) { $value = $price['website_price']; } else { - $value = $price['price']; + $value = $price['price'] ?? 0; } $tierPrice->setValue($value); $tierPrice->setQty($price['price_qty']); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php index 203126cf1fd8c..03f1edea0ea77 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php @@ -101,7 +101,7 @@ protected function _clearUselessAttributeValues(\Magento\Framework\Model\Abstrac $attributeStoreIds = array_keys($this->_storeManager->getStores()); if (!empty($attributeStoreIds)) { $delCondition = [ - 'attribute_id = ?' => $object->getId(), + 'attribute_id = ?' => (int)$object->getId(), 'store_id IN(?)' => $attributeStoreIds, ]; $this->getConnection()->delete($object->getBackendTable(), $delCondition); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index e19286efc38c0..ed2df0f10ac3b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -575,7 +575,8 @@ public function verifyIds(array $ids) 'entity_id' )->where( 'entity_id IN(?)', - $ids + $ids, + \Zend_Db::INT_TYPE ); return $this->getConnection()->fetchCol($select); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Url.php b/app/code/Magento/Catalog/Model/ResourceModel/Url.php index be95f088a2477..eceae322fbd8e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Url.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Url.php @@ -204,7 +204,8 @@ protected function _getCategoryAttribute($attributeCode, $categoryIds, $storeId) ['value' => $attributeCode, 'entity_id' => 'entity_id'] )->where( 'entity_id IN(?)', - $categoryIds + $categoryIds, + \Zend_Db::INT_TYPE ); } elseif ($this->_categoryAttributes[$attributeCode]['is_global'] || $storeId == 0) { $select->from( @@ -216,7 +217,8 @@ protected function _getCategoryAttribute($attributeCode, $categoryIds, $storeId) ['value'] )->where( "t1.{$identifierFiled} IN(?)", - $categoryIds + $categoryIds, + \Zend_Db::INT_TYPE )->where( 'e.attribute_id = :attribute_id' )->where( @@ -245,7 +247,8 @@ protected function _getCategoryAttribute($attributeCode, $categoryIds, $storeId) 't1.attribute_id = :attribute_id' )->where( "e.entity_id IN(?)", - $categoryIds + $categoryIds, + \Zend_Db::INT_TYPE )->group('e.entity_id'); $bind['attribute_id'] = $this->_categoryAttributes[$attributeCode]['attribute_id']; @@ -308,7 +311,8 @@ public function _getProductAttribute($attributeCode, $productIds, $storeId) 0 )->where( 'entity_id IN(?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); } else { $valueExpr = $connection->getCheckSql('t2.value_id > 0', 't2.value', 't1.value'); @@ -326,7 +330,8 @@ public function _getProductAttribute($attributeCode, $productIds, $storeId) 't1.attribute_id = :attribute_id' )->where( 't1.entity_id IN(?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); $bind['store_id'] = $storeId; } @@ -430,7 +435,7 @@ protected function _getCategories($categoryIds, $storeId = null, $path = null) // Prepare variables for checking whether categories belong to store if ($path === null) { - $select->where('main_table.entity_id IN(?)', $categoryIds); + $select->where('main_table.entity_id IN(?)', $categoryIds, \Zend_Db::INT_TYPE); } else { // Ensure that path ends with '/', otherwise we can get wrong results - e.g. $path = '1/2' will get '1/20' if (substr($path, -1) != '/') { @@ -569,7 +574,7 @@ protected function _getProducts($productIds, $storeId, $entityId, &$lastEntityId $this->_productLimit ); if ($productIds !== null) { - $select->where('e.entity_id IN(?)', $productIds); + $select->where('e.entity_id IN(?)', $productIds, \Zend_Db::INT_TYPE); } $rowSet = $connection->fetchAll($select, $bind); @@ -591,7 +596,8 @@ protected function _getProducts($productIds, $storeId, $entityId, &$lastEntityId ['product_id', 'category_id'] )->where( 'product_id IN(?)', - array_keys($products) + array_keys($products), + \Zend_Db::INT_TYPE ); $categories = $connection->fetchAll($select); foreach ($categories as $category) { diff --git a/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php index ca87efaa87490..bdee84762cac2 100644 --- a/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php +++ b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Observer; use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Catalog\Model\Indexer\Category\Flat\State as FlatState; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; @@ -21,12 +22,21 @@ class CategoryProductIndexer implements ObserverInterface */ private $processor; + /** + * @var FlatState + */ + private $flatState; + /** * @param Processor $processor + * @param FlatState $flatState */ - public function __construct(Processor $processor) - { + public function __construct( + Processor $processor, + FlatState $flatState + ) { $this->processor = $processor; + $this->flatState = $flatState; } /** @@ -35,7 +45,7 @@ public function __construct(Processor $processor) public function execute(Observer $observer): void { $productIds = $observer->getEvent()->getProductIds(); - if (!empty($productIds) && $this->processor->isIndexerScheduled()) { + if (!empty($productIds) && $this->processor->isIndexerScheduled() && $this->flatState->isFlatEnabled()) { $this->processor->markIndexerAsInvalid(); } } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignTwoCategoriesToProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignTwoCategoriesToProductActionGroup.xml new file mode 100644 index 0000000000000..06b4b68630326 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignTwoCategoriesToProductActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + Extends AdminAssignCategoryToProductAndSaveActionGroup + assigns the second category and prevents product saving (the Product Edit page should be opened in Admin prior this check). + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckNameToggleOnProductsMassAttributeUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckNameToggleOnProductsMassAttributeUpdateActionGroup.xml new file mode 100644 index 0000000000000..919e1bcb0dcb2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckNameToggleOnProductsMassAttributeUpdateActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + Click the "Change" checkbox for the "Name" field on the Products Masss Attributes Update page. + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductOnProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductOnProductGridActionGroup.xml new file mode 100644 index 0000000000000..64fab5575e392 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductOnProductGridActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + Check the checkbox for the product on the Product Grid + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml index 90cc7666eb92f..ec3d26e8a3f36 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml @@ -14,6 +14,7 @@ - + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickSaveOnProductsMassAttributeUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickSaveOnProductsMassAttributeUpdateActionGroup.xml new file mode 100644 index 0000000000000..a3328c5eb115b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickSaveOnProductsMassAttributeUpdateActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + Clicks on 'Save' button on products mass attributes update page. + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml new file mode 100644 index 0000000000000..fe5b0ae1a64ce --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml @@ -0,0 +1,35 @@ + + + + + + Open product attributes grid filter it by attribute code and delete all found attributes one by one. + + + + + + + + + + + + + {{AdminDataGridTableSection.firstNotEmptyRow2}} + {{AdminConfirmationModalSection.ok}} + {{AdminMainActionsSection.delete}} + {{AdminMessagesSection.success}} + You deleted the product attribute. + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillMainProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillMainProductFormActionGroup.xml new file mode 100644 index 0000000000000..c5818a4eea51b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillMainProductFormActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + Extends FillMainProductFormActionGroup with filling the next fields: Tax Class, Visibility, SEO->URL + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyIncrementsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyIncrementsActionGroup.xml new file mode 100644 index 0000000000000..a39ac8c3f5d2b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyIncrementsActionGroup.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSearchGridByStringNoClearActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSearchGridByStringNoClearActionGroup.xml new file mode 100644 index 0000000000000..afaa3c28c56e8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSearchGridByStringNoClearActionGroup.xml @@ -0,0 +1,23 @@ + + + + + + + Search the Admin grid by string without clearing filters. + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusActionGroup.xml new file mode 100644 index 0000000000000..05f5a90cc97a7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusActionGroup.xml @@ -0,0 +1,23 @@ + + + + + + + Set Stock Status of product. + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminManageStockOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminManageStockOnEditPageActionGroup.xml new file mode 100644 index 0000000000000..eca989a5de41f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminManageStockOnEditPageActionGroup.xml @@ -0,0 +1,23 @@ + + + + + + + Check if manageStock value is correct + (the Product Edit page->Advanced Inventory section should be opened in Admin prior this check). + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductInfoOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductInfoOnEditPageActionGroup.xml new file mode 100644 index 0000000000000..d91cdfee0489c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductInfoOnEditPageActionGroup.xml @@ -0,0 +1,33 @@ + + + + + + + Validates next fields on the Product Edit Page: + name, sku, price, quantity, stock status, tax class, weight, weigh select, visibility, url key + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductIsAssignedToCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductIsAssignedToCategoryActionGroup.xml new file mode 100644 index 0000000000000..e6b178937a2cc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductIsAssignedToCategoryActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + Checks if product is assigned to category (the Product Edit page should be opened in Admin prior this check). + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductStockStatusOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductStockStatusOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..857d88ebc197c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductStockStatusOnProductPageActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + Validates that the provided Product Stock Status is present and correct + (the Product Detail page should be opened on Storefront prior this check) + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index f4e468a939111..5375459122e69 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -586,6 +586,10 @@ ProductOptionValueDropdown + + + ProductOptionFile + ProductOptionValueDropdown diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml index 731754ef01959..b763fda489b20 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -80,4 +80,10 @@ 1 + + All Websites [USD] + ALL GROUPS + 0.1 + 1 + diff --git a/app/code/Magento/Catalog/Test/Mftf/Helper/CatalogHelper.php b/app/code/Magento/Catalog/Test/Mftf/Helper/CatalogHelper.php new file mode 100644 index 0000000000000..dcba3b1bf68de --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Helper/CatalogHelper.php @@ -0,0 +1,59 @@ +getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + /** @var FacebookWebDriver $webDriver */ + $webDriver = $magentoWebDriver->webDriver; + $gridRows = $webDriver->findElements(WebDriverBy::cssSelector($notEmptyRow)); + while (!empty($gridRows)) { + $gridRows[0]->click(); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForElementVisible($successMessageContainer); + $magentoWebDriver->see($successMessage, $successMessageContainer); + $gridRows = $webDriver->findElements(WebDriverBy::cssSelector($notEmptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml index 1ca051e2f6669..d70c48f2b00e3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml @@ -77,5 +77,6 @@ + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml index 92dadbdd26c2d..9881545fac9cd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml @@ -13,5 +13,11 @@ + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml index 45284e69a54e0..0b29d2edb6615 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml @@ -23,6 +23,7 @@ 100 + @@ -31,29 +32,46 @@ - + + - - - - - - - - + + + + + + + + + + + + + + + + + - - + + + + + - - + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml index 8d41b276334a6..dd98df9325665 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml @@ -20,6 +20,9 @@ - + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml index 9c1ff43587a27..db789d3512acf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -69,10 +69,7 @@ - - - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml index 3332bc66653e5..8d0534891a29b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml @@ -70,10 +70,7 @@ - - - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml index 500c95d1120f3..7447c75a778af 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml @@ -30,10 +30,7 @@ - - - - + @@ -47,9 +44,7 @@ - - - + @@ -68,10 +63,7 @@ - - - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml index 2394b41502f84..df2124759686d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -30,10 +30,7 @@ - - - - + @@ -47,9 +44,7 @@ - - - + @@ -69,9 +64,8 @@ - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml index 35e53273aebf2..2f86209da1eba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -30,10 +30,7 @@ - - - - + @@ -47,9 +44,7 @@ - - - + @@ -70,9 +65,8 @@ - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml index 61ef389c7909e..37a4bc613f65d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -53,7 +53,9 @@ - + + + @@ -88,10 +90,7 @@ - - - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml index 9a9d64617f7b5..573877f9869d4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml @@ -50,7 +50,9 @@ - + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml index 54c3a05651c44..a00714e412b0a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml @@ -31,9 +31,7 @@ - - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 7871f4305575a..3141db87f6976 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -117,9 +117,7 @@ - - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml index 994ea76ca33df..f2840758d59a6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -95,10 +95,7 @@ - - - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml index 7e5ee977d679b..3514f53e8b937 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml @@ -65,9 +65,7 @@ - - - + @@ -96,10 +94,7 @@ - - - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberSetsToOneAfterNewSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberSetsToOneAfterNewSearchTest.xml new file mode 100644 index 0000000000000..9bb7064b1870b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberSetsToOneAfterNewSearchTest.xml @@ -0,0 +1,105 @@ + + + + + + + + + + <description value="When changing the search keyword in admin product grid, new results should be displayed from the page one"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-39332"/> + <useCaseId value="MC-38787"/> + <group value="Catalog"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <comment userInput="Clear product grid" stepKey="commentClearProductGrid"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridToDefaultView"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProductIfTheyExist"/> + + <!-- Create required prerequisites --> + <createData entity="SimpleSubCategory" stepKey="category1"/> + <createData entity="SimpleProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct3"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct4"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="VirtualProduct" stepKey="virtualProduct1"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="VirtualProduct" stepKey="virtualProduct2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="VirtualProduct" stepKey="virtualProduct3"> + <requiredEntity createDataKey="category1"/> + </createData> + </before> + + <after> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + <actionGroup ref="AdminDataGridDeleteCustomPerPageActionGroup" stepKey="deleteCustomAddedPerPage"> + <argument name="perPage" value="ProductPerPage.productCount"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + + <!-- Delete prerequisites --> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> + <deleteData createDataKey="virtualProduct2" stepKey="deleteVirtualProduct2"/> + <deleteData createDataKey="virtualProduct3" stepKey="deleteVirtualProduct3"/> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + + <!-- Set the product grid to display one product per page --> + <actionGroup ref="AdminDataGridSelectCustomPerPageActionGroup" stepKey="select1ProductPerPage"> + <argument name="perPage" value="ProductPerPage.productCount"/> + </actionGroup> + + <!-- Performing the first search and assertions --> + <actionGroup ref="AdminSearchGridByStringNoClearActionGroup" stepKey="searchForSimpleProduct"> + <argument name="keyword" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="AdminGridAssertTotalPageCountActionGroup" stepKey="waitForTotalPagesCountFourToBeVisible"> + <argument name="expectedTotalPageCount" value="4"/> + </actionGroup> + <actionGroup ref="AdminGridGoToNextPageActionGroup" stepKey="clickNextPageProductGrid"/> + <actionGroup ref="AdminGridAssertCurrentPageNumberActionGroup" stepKey="assertCurrentPageIsTwoOnProductGridFirstSearch"> + <argument name="expectedCurrentPageNumber" value="2"/> + </actionGroup> + + <!-- Performing the second search and assertions of successful current page number reset --> + <actionGroup ref="AdminSearchGridByStringNoClearActionGroup" stepKey="searchForVirtualProduct"> + <argument name="keyword" value="VirtualProduct"/> + </actionGroup> + <actionGroup ref="AdminGridAssertTotalPageCountActionGroup" stepKey="waitForTotalPagesCountThreeToBeVisible"> + <argument name="expectedTotalPageCount" value="3"/> + </actionGroup> + <actionGroup ref="AdminGridAssertCurrentPageNumberActionGroup" stepKey="assertCurrentPageIsOneOnProductGridSecondSearch"> + <argument name="expectedCurrentPageNumber" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml index af31b7c1d5c07..4dd76e55f9330 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -31,10 +31,7 @@ <requiredEntity createDataKey="createCategory"/> </createData> - <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml index 479c9e5e5bb19..1871c2acf66e1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml @@ -36,20 +36,27 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> - <!-- Search and select products --> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> - <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox1"/> - <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> - <!-- Mass update attributes --> - <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> - <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> - <waitForPageLoad stepKey="waitForBulkUpdatePage"/> - <seeInCurrentUrl stepKey="seeInUrl" url="catalog/product_action_attribute/edit/"/> - <click selector="{{AdminEditProductAttributesSection.ChangeAttributeNameToggle}}" stepKey="toggleToChangeName"/> - <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> + + <actionGroup ref="AdminCheckProductOnProductGridActionGroup" stepKey="clickCheckbox1"> + <argument name="product" value="$$createProductOne$$"/> + </actionGroup> + + <actionGroup ref="AdminCheckProductOnProductGridActionGroup" stepKey="clickCheckbox2"> + <argument name="product" value="$$createProductTwo$$"/> + </actionGroup> + + <actionGroup ref="AdminClickMassUpdateProductAttributesActionGroup" stepKey="clickDropdown"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickOption"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForBulkUpdatePage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="seeInUrl"/> + + <actionGroup ref="AdminCheckNameToggleOnProductsMassAttributesUpdateActionGroup" stepKey="toggleToChangeName"/> + + <actionGroup ref="AdminClickSaveOnProductsMassAttributesUpdateActionGroup" stepKey="save"/> <see stepKey="seeError" selector="{{AdminEditProductAttributesSection.NameError}}" userInput="This is a required field"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml new file mode 100644 index 0000000000000..aa82b092a0a98 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMassUpdateProductQtyIncrementsTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update advanced inventory attributes"/> + <title value="Admin should be able to mass update product qty increments"/> + <description value="Admin should be able to mass update product qty increments"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-39359"/> + <useCaseId value="MC-36787"/> + <group value="catalog"/> + <group value="CatalogInventory"/> + <group value="product_attributes"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProductOne"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProductTwo"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> + <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + </after> + <!-- Navigate to products list page and select created products --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> + <argument name="keyword" value="api-simple-product"/> + </actionGroup> + <actionGroup ref="SortProductsByIdDescendingActionGroup" stepKey="sortProductsByIdDescending"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox1"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> + <!-- Mass update qty increments --> + <actionGroup ref="AdminClickMassUpdateProductAttributesActionGroup" stepKey="clickMassUpdateProductAttributes"/> + <actionGroup ref="AdminMassUpdateProductQtyIncrementsActionGroup" stepKey="updateQtyIncrements"> + <argument name="enableQtyIncrements" value="Yes"/> + <argument name="qtyIncrements" value="2"/> + </actionGroup> + <!-- Start message queue for product attribute consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminProductAttributeUpdateMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminProductAttributeUpdateMessageConsumerData.messageLimit}}"/> + </actionGroup> + <!-- Open first product for edit and assert that qty increment is updated --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createProductOne.id$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <seeOptionIsSelected selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" userInput="Yes" stepKey="assertEnableQtyIncrementsValue"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="assertEnableQtyIncrementsUseConfigSettings"/> + <seeInField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput="2" stepKey="assertQtyIncrementsValue"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="assertQtyIncrementsUseConfigSettings"/> + <!-- Open second product for edit and assert that qty increment is updated --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage2"> + <argument name="productId" value="$createProductTwo.id$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink2"/> + <seeOptionIsSelected selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" userInput="Yes" stepKey="assertEnableQtyIncrementsValue2"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="assertEnableQtyIncrementsUseConfigSettings2"/> + <seeInField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput="2" stepKey="assertQtyIncrementsValue2"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="assertQtyIncrementsUseConfigSettings2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml index 809a015369ea9..3da19eb598012 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -65,10 +65,7 @@ <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> - <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml index 9100e6027a52f..1c55b09151cf3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml @@ -22,10 +22,7 @@ <createData entity="FirstLevelSubCat" stepKey="createDefaultCategory"> <field key="is_active">true</field> </createData> - <!-- Perform reindex and flush cache --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml index 0e056e4bb7078..fe3ffbb4fc1d7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -56,10 +56,7 @@ <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> - <!--Run re-index task --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Verify category displayed in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml index 8e728fc6e1f27..e06a7f3c5679c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml @@ -34,10 +34,7 @@ <argument name="customStore" value="storeViewData"/> </actionGroup> - <!--Run full reindex and clear caches --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml index 1707fda9e3edb..e989aa3758cf3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -65,9 +65,7 @@ </actionGroup> <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml index 73aeed3af4fb0..f5046faf82b6f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml @@ -45,9 +45,7 @@ <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml index 208b588493112..f7f87da77b401 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml @@ -35,9 +35,7 @@ <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> @@ -48,9 +46,7 @@ <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="indexerReindex"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> </actionGroup> @@ -61,10 +57,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!-- Select Created Category--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexBeforeFlow"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexBeforeFlow"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> @@ -82,10 +75,7 @@ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> - <!--Open Index Management Page and verify flat categoryIndex status--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml index a688dea47a0c4..b316e3194c986 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -64,9 +64,8 @@ <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="enableIncludeInMenuOption"/> <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> - <!--Run full reindex and clear caches --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <argument name="indices" value="catalog_category_flat"/> </actionGroup> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml index 5f7cecfde188a..70d6932f6fc17 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -42,12 +42,12 @@ </after> <!-- Search default simple product in grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Assign simple product to created store view --> <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="clickCategoryStoreViewDropdownToggle"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml index 8a05ed9d64b8b..f9c90b1190611 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -42,12 +42,12 @@ </after> <!-- Search default simple product in grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Assign simple product to created store view --> <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="clickCategoryStoreViewDropdownToggle"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml index 300b312612253..53d9beb6169ee 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml @@ -44,12 +44,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with tier price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="fillSimpleProductName"/> @@ -110,8 +110,12 @@ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductTierPrice300InStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductTierPrice300InStock.weight}}" stepKey="seeSimpleProductWeight"/> <seeInField selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductTierPrice300InStock.weightSelect}}" stepKey="seeSimpleProductWeightSelect"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories"/> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="seeSelectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductTierPrice300InStock.urlKey}}" stepKey="seeUrlKey"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml index a9630aba467c6..9cae9cc3f1f42 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml @@ -34,12 +34,12 @@ </after> <!-- Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDisabled.name}}" stepKey="fillSimpleProductName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml new file mode 100644 index 0000000000000..3af6de07e561d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) Enabled Flat"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Enabled Flat"/> + <testCaseId value="MC-10818"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 1"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI stepKey="unsetFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 0"/> + </after> + + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProductPage"> + <argument name="product" value="$$initialSimpleProduct$$"/> + </actionGroup> + + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickAdvancedInventoryLink"/> + <actionGroup ref="AdminSetManageStockConfigActionGroup" stepKey="setManageStockConfig"> + <argument name="value" value="No"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + + <actionGroup ref="AdminAssignTwoCategoriesToProductActionGroup" stepKey="assignCategories"> + <argument name="categoryName" value="$$initialCategoryEntity.name$$"/> + <argument name="categoryTwoName" value="$$categoryEntity.name$$"/> + </actionGroup> + + <actionGroup ref="AdminFillMainProductFormActionGroup" stepKey="fillSimpleProductInfo"> + <argument name="product" value="simpleProductEnabledFlat"/> + </actionGroup> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> + + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeSimpleProductSavedSuccessMessage"/> + + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProductPage1"> + <argument name="product" value="simpleProductEnabledFlat"/> + </actionGroup> + + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickTheAdvancedInventoryLink1"/> + <actionGroup ref="AssertAdminManageStockOnEditPageActionGroup" stepKey="assertManageStock1"> + <argument name="manageStock" value="No"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButtonOnAdvancedInventorySection1"/> + + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="checkifProductIsAssignedToInitialCategory"> + <argument name="categoryName" value="$$initialCategoryEntity.name$$"/> + </actionGroup> + + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="checkifProductIsAssignedToCategoryTwo"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + + <actionGroup ref="AssertAdminProductInfoOnEditPageActionGroup" stepKey="assertProductInfo"> + <argument name="product" value="simpleProductEnabledFlat"/> + </actionGroup> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="category" value="$categoryEntity$"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="seeSimpleProductNameOnCategoryPage"> + <argument name="productName" value="{{simpleProductEnabledFlat.name}}"/> + </actionGroup> + + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage"> + <argument name="productUrlKey" value="{{simpleProductEnabledFlat.urlKey}}"/> + </actionGroup> + + <actionGroup ref="StorefrontAssertProductNameOnProductPageActionGroup" stepKey="seeSimpleProductNameOnStoreFrontPage"> + <argument name="productName" value="{{simpleProductEnabledFlat.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPriceOnProductPageActionGroup" stepKey="seeSimpleProductPriceOnStoreFrontPage"> + <argument name="productPrice" value="{{simpleProductEnabledFlat.price}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSKUOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductStockStatusOnProductPageActionGroup" stepKey="seeSimpleProductStockStatusOnStoreFrontPage"> + <argument name="productStockStatus" value="{{simpleProductEnabledFlat.storefrontStatus}}"/> + </actionGroup> + + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchForSku"> + <argument name="phrase" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearchActionGroup" stepKey="openAndCheckProduct"> + <argument name="productName" value="{{simpleProductEnabledFlat.name}}"/> + <argument name="productUrlKey" value="{{simpleProductEnabledFlat.urlKey}}"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml index aa1b1ae702914..d2e145adb6a86 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml @@ -8,15 +8,18 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest"> + <test name="AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest" deprecated="Use AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest instead"> <annotations> <stories value="Update Simple Product"/> - <title value="Update Simple Product with Regular Price (In Stock) Enabled Flat"/> + <title value="DEPRECACTED. Update Simple Product with Regular Price (In Stock) Enabled Flat"/> <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Enabled Flat"/> <testCaseId value="MC-10818"/> <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -38,12 +41,12 @@ </after> <!-- Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="fillSimpleProductName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml index 86fac835ce44d..aad946dced83c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,12 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductNotVisibleIndividually.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductNotVisibleIndividually.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductNotVisibleIndividually.weightNoDecimals}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="seeSelectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductNotVisibleIndividually.visibility}}" stepKey="seeSimpleProductVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSectionHeader"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml index af3861e4e0b64..7bb5f090edf5d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml @@ -34,12 +34,12 @@ </after> <!--Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product by unselecting categories --> <scrollTo selector="{{AdminProductFormSection.productStockStatus}}" stepKey="scroll"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml index 320edba5feeff..c5f6e70be046c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,12 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice245InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice245InStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice245InStock.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice245InStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml index 77c3e7548a3cf..c3f7240537293 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,12 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32501InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice32501InStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32501InStock.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice32501InStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml index 39dab0b08915c..4ce3af5991235 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,12 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice325InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice325InStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice325InStock.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice325InStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml index 670030d1d98ea..2316e36eb0b9c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductName"/> @@ -97,8 +97,12 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPriceCustomOptions.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPriceCustomOptions.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPriceCustomOptions.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="seeUrlKey"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml index 441bc9b8f8005..dcec3e3f8bd50 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(out of stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,12 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32503OutOfStock.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice32503OutOfStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32503OutOfStock.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="seeUrlKey"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml index 507e4ae14e83c..2f38e16a44476 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml @@ -44,7 +44,7 @@ </actionGroup> <actionGroup ref="AdminSetQuantityToEachSkusConfigurableProductActionGroup" stepKey="saveConfigurable"/> <grabValueFrom selector="{{NewProductPageSection.sku}}" stepKey="grabSkuProduct"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <comment userInput="Adding the comment to replace 'indexer:reindex' command for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="expandOption"> <argument name="categoryName" value="$$createCategory.name$$"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml index dc608a7f12dd3..2063054a94d0b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml @@ -23,7 +23,7 @@ <requiredEntity createDataKey="createCategory"/> </createData> <magentoCLI command="config:set {{CatalogInventoryOptionsOnlyXleftThreshold.path}} 10000" stepKey="setStockThresholdQty"/> - <magentoCLI command="cache:flush config" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml index 67ca04a0a4594..1032c322053da 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml @@ -39,10 +39,7 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <!--Run re-index task--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml index 2080aee933aad..814356bb05a22 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -48,6 +48,8 @@ </before> <after> + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> @@ -70,7 +72,7 @@ <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <comment userInput="BIC workaround" stepKey="customerLogoutStorefront"/> </after> <!-- Open Product Grid, Filter product and open --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index 631d1d50077e9..71041ee7e1349 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -31,14 +31,15 @@ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> </before> <after> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete product and category --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderListingFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> - <!-- Logout customer --> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <comment userInput="BIC workaround" stepKey="customerLogoutStorefront"/> </after> <!-- Login Customer Storefront --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml index aac76999636b0..ce419167e9514 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml @@ -33,6 +33,8 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php index 09ad8bb41de7c..c14bb7f524d03 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php @@ -263,4 +263,40 @@ function () { ); } } + + /** + * Get tier price with percent value type + * + * @return void + */ + public function testGetPricesWithPercentType(): void + { + $tierPrices = [ + 0 => [ + 'record_id' => 0, + 'cust_group' => 3200, + 'price_qty' => 3, + 'website_id' => 0, + 'value_type' => 'percent', + 'percentage_value' => 10, + ], + ]; + $this->product->setData('tier_price', $tierPrices); + $this->tpFactory->expects($this->any()) + ->method('create') + ->willReturnCallback( + function () { + return $this->objectManagerHelper->getObject(TierPrice::class); + } + ); + $tierPriceExtensionMock = $this->getMockBuilder(ProductTierPriceExtensionInterface::class) + ->onlyMethods(['getPercentageValue', 'setPercentageValue']) + ->getMockForAbstractClass(); + $tierPriceExtensionMock->method('getPercentageValue') + ->willReturn(50); + $this->tierPriceExtensionFactoryMock->method('create') + ->willReturn($tierPriceExtensionMock); + + $this->assertInstanceOf(TierPrice::class, $this->model->getTierPrices($this->product)[0]); + } } diff --git a/app/code/Magento/Catalog/etc/mview.xml b/app/code/Magento/Catalog/etc/mview.xml index 7ae38a7f2d0e1..2c9d7d448afd2 100644 --- a/app/code/Magento/Catalog/etc/mview.xml +++ b/app/code/Magento/Catalog/etc/mview.xml @@ -49,6 +49,7 @@ <table name="catalog_product_entity_decimal" entity_column="entity_id" /> <table name="catalog_product_entity_int" entity_column="entity_id" /> <table name="catalog_product_entity_tier_price" entity_column="entity_id" /> + <table name="catalog_product_link" entity_column="product_id" /> </subscriptions> </view> <view id="catalog_product_attribute" class="Magento\Catalog\Model\Indexer\Product\Eav" group="indexer"> @@ -56,6 +57,7 @@ <table name="catalog_product_entity_decimal" entity_column="entity_id" /> <table name="catalog_product_entity_int" entity_column="entity_id" /> <table name="catalog_product_entity_varchar" entity_column="entity_id" /> + <table name="catalog_product_link" entity_column="product_id" /> </subscriptions> </view> </config> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml index 5ca88689b9e5f..d786f843e052f 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml @@ -10,11 +10,6 @@ $blockId = $block->getId(); <div id="product_composite_configure" class="product-configure-popup product-configure-popup-<?= $block->escapeHtmlAttr($blockId) ?>"> <iframe name="product_composite_configure_iframe" id="product_composite_configure_iframe"></iframe> - <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( - 'onload', - "window.productConfigure && productConfigure.onLoadIFrame()", - 'iframe[name=\'product_composite_configure_iframe\']:last-of-type' - ) ?> <form action="" method="post" id="product_composite_configure_form" enctype="multipart/form-data" target="product_composite_configure_iframe" class="product_composite_configure_form"> @@ -85,3 +80,8 @@ script; ?> <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> </div> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onload', + "window.productConfigure && productConfigure.onLoadIFrame()", + 'iframe[name=\'product_composite_configure_iframe\']:last-of-type' +) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml index 344123cbe5640..646512d98582e 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml @@ -334,7 +334,7 @@ if (!is_numeric($defaultMinSaleQty)) { </div> <div class="field choice"> <input type="checkbox" id="inventory_use_config_enable_qty_increments" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_enable_qty_increments]" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_enable_qty_inc]" value="1" data-role="toggle-editability" checked="checked" diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js index 1ac2a4ffadaae..4040ff9d684f4 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js @@ -607,6 +607,7 @@ define([ * @param method can be 'item_confirm', 'item_restore', 'current_confirmed_to_form', 'form_confirmed_to_confirmed' */ _processFieldsData: function (method) { + var self = this; /** * Internal function for rename fields names of some list type @@ -616,12 +617,14 @@ define([ * @param blockItem */ var _renameFields = function (method, blockItem, listType) { - var pattern = null; - var patternFlat = null; - var replacement = null; - var replacementFlat = null; - var scopeArr = blockItem.id.match(/.*\[\w+\]\[([^\]]+)\]$/); - var itemId = scopeArr[1]; + var pattern = null; + var patternFlat = null; + var patternPrefix = RegExp('\\s', 'g'); + var replacement = null; + var replacementFlat = null; + var replacementPrefix = '_'; + var scopeArr = blockItem.id.match(/.*\[\w+\]\[([^\]]+)\]$/); + var itemId = scopeArr[1]; if (method == 'current_confirmed_to_form') { pattern = RegExp('(\\w+)(\\[?)'); @@ -651,6 +654,14 @@ define([ var rename = function (elms) { for (var i = 0; i < elms.length; i++) { if (elms[i].name && elms[i].type == 'file') { + var prefixName = 'options[files_prefix]', + prefixValue = 'item_' + itemId + '_'; + + self.blockFormFields.insert(new Element('input', { + type: 'hidden', + name: prefixName.replace(pattern, replacement), + value: prefixValue.replace(patternPrefix, replacementPrefix) + })); elms[i].name = elms[i].name.replace(patternFlat, replacementFlat); } else if (elms[i].name) { elms[i].name = elms[i].name.replace(pattern, replacement); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php index b0f085932bb8e..201c70913ca39 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php @@ -148,7 +148,7 @@ private function getAttributesFromEntityTable( ): Select { $select = $connection->select() ->from(['e' => $entityTableName], $entityTableAttributes) - ->where('e.entity_id IN (?)', $entityIds); + ->where('e.entity_id IN (?)', $entityIds, \Zend_Db::INT_TYPE); return $select; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php index 805571d58d634..ed5ae433dd5b7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php @@ -63,6 +63,13 @@ public function resolve( $product = $value['model']; $product->unsetData('minimal_price'); + if ($context) { + $customerGroupId = $context->getExtensionAttributes()->getCustomerGroupId(); + if ($customerGroupId !== null) { + $product->setCustomerGroupId($customerGroupId); + } + } + $requestedFields = $info->getFieldSelection(10); $returnArray = []; diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php index 992ab50467c72..b61ecfff4e3f1 100644 --- a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -106,6 +106,9 @@ private function getSearchableAttributes(): array $productAttributes->addFieldToFilter( ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'], [1, 1, [1, 2], 1] + )->setOrder( + 'position', + 'ASC' ); /** @var Attribute $attribute */ diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 425d1309bd46c..673dbcb3b3c99 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1807,7 +1807,7 @@ protected function _saveProducts() if ($column === self::COL_MEDIA_IMAGE) { $rowData[$column][] = $uploadedFile; } - $mediaGallery[$storeId][$rowSku][$uploadedFile] = [ + $mediaGalleryStoreData = [ 'attribute_id' => $this->getMediaGalleryAttributeId(), 'label' => isset($rowLabels[$column][$columnImageKey]) ? $rowLabels[$column][$columnImageKey] @@ -1817,6 +1817,15 @@ protected function _saveProducts() ? $imageHiddenStates[$columnImage] : '0', 'value' => $uploadedFile, ]; + $mediaGallery[$storeId][$rowSku][$uploadedFile] = $mediaGalleryStoreData; + // Add record for default scope if it does not exist + if (!($mediaGallery[Store::DEFAULT_STORE_ID][$rowSku][$uploadedFile] ?? [])) { + //Set label and disabled values to their default values + $mediaGalleryStoreData['label'] = null; + $mediaGalleryStoreData['disabled'] = 0; + $mediaGallery[Store::DEFAULT_STORE_ID][$rowSku][$uploadedFile] = $mediaGalleryStoreData; + } + } } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index d4694b72ba64f..c838688c1c4f8 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -12,6 +12,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\Store\Model\Store; /** * Process and saves images during import. @@ -259,7 +260,10 @@ private function prepareMediaGalleryValueData( $position = $data['position']; $storeId = $data['store_id']; $mediaGalleryValueData[$index]['value_id'] = $productIdMediaValueIdMap[$productId][$value]; - $mediaGalleryValueData[$index]['position'] = $position + ($lastPositions[$storeId][$productId] ?? 0); + $lastPosition = $lastPositions[$storeId][$productId] + ?? $lastPositions[Store::DEFAULT_STORE_ID][$productId] + ?? 0; + $mediaGalleryValueData[$index]['position'] = $position + $lastPosition; unset($mediaGalleryValueData[$index]['value']); } return $mediaGalleryValueData; diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php index 2ad7ca9f14963..2b37c9099a0e6 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php @@ -105,7 +105,7 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = } if (!empty($entityIds)) { - $select->where('stock_item.product_id in (?)', $entityIds, \Zend_Db::INT_TYPE); + $select->where('stock_item.product_id IN (?)', $entityIds, \Zend_Db::INT_TYPE); } $select->group('stock_item.product_id'); @@ -121,7 +121,7 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = foreach ($batchSelectIterator as $select) { $productIds = null; foreach ($connection->query($select)->fetchAll() as $row) { - $productIds[] = $row['product_id']; + $productIds[] = (int) $row['product_id']; } if ($productIds !== null) { $where = [$priceTable->getEntityField() .' IN (?)' => $productIds]; diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php index 4ea6b6bcfde9a..0b5f248331bfd 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php @@ -167,7 +167,7 @@ public function getRelationsByChild($childIds) )->join( ['relation' => $this->_getTable('catalog_product_relation')], 'relation.parent_id = cpe.' . $linkField - )->where('child_id IN(?)', $childIds); + )->where('child_id IN(?)', $childIds, \Zend_Db::INT_TYPE); return $connection->fetchCol($select); } @@ -262,7 +262,7 @@ private function doReindex($productIds = []) // retrieve product types by processIds $select = $connection->select() ->from($this->_getTable('catalog_product_entity'), ['entity_id', 'type_id']) - ->where('entity_id IN(?)', $productIds); + ->where('entity_id IN(?)', $productIds, \Zend_Db::INT_TYPE); $pairs = $connection->fetchPairs($select); $byType = []; diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php index c151e5897abd5..dec18044b699e 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php @@ -261,7 +261,7 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f $select->columns(['status' => $this->getStatusExpression($connection, true)]); if ($entityIds !== null) { - $select->where('e.entity_id IN(?)', $entityIds); + $select->where('e.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } return $select; diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index 02e443d09b228..afb7d51335df8 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -153,7 +153,7 @@ public function getProductsStockStatuses($productIds, $websiteId, $stockId = Sto $select = $this->getConnection()->select() ->from($this->getMainTable(), ['product_id', 'stock_status']) - ->where('product_id IN(?)', $productIds) + ->where('product_id IN(?)', $productIds, \Zend_Db::INT_TYPE) ->where('stock_id=?', (int) $stockId) ->where('website_id=?', (int) $websiteId); return $this->getConnection()->fetchPairs($select); @@ -190,7 +190,8 @@ public function getProductsType($productIds) ['entity_id', 'type_id'] )->where( 'entity_id IN(?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); return $this->getConnection()->fetchPairs($select); } @@ -360,7 +361,8 @@ public function getProductStatus($productIds, $storeId = null) $attribute->getAttributeId() )->where( "t1.{$linkField} IN(?)", - $productIds + $productIds, + \Zend_Db::INT_TYPE ); $rows = $connection->fetchPairs($select); diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index 2dd47eae16959..9351abf0ead3d 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -64,6 +64,14 @@ class MassUpdateProductAttribute * @var ProductRepositoryInterface */ private $productRepository; + + /** + * @var array + */ + private $useConfigFieldMap = [ + 'enable_qty_increments' => 'use_config_enable_qty_inc' + ]; + /** * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper @@ -146,7 +154,9 @@ private function addConfigSettings($inventoryData) { $options = $this->stockConfiguration->getConfigItemOptions(); foreach ($options as $option) { - $useConfig = 'use_config_' . $option; + $useConfig = isset($this->useConfigFieldMap[$option]) + ? $this->useConfigFieldMap[$option] + : 'use_config_' . $option; if (isset($inventoryData[$option]) && !isset($inventoryData[$useConfig])) { $inventoryData[$useConfig] = 0; } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php index 0f3d8be212e30..090a74afca050 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php @@ -82,7 +82,7 @@ public function testModifyPrice() $connectionMock->expects($this->once())->method('select')->willReturn($selectMock); $selectMock->expects($this->at(2)) ->method('where') - ->with('stock_item.product_id in (?)', $entityIds) + ->with('stock_item.product_id IN (?)', $entityIds) ->willReturn($selectMock); $this->generator->expects($this->once()) ->method('generate') diff --git a/app/code/Magento/CatalogInventory/etc/mview.xml b/app/code/Magento/CatalogInventory/etc/mview.xml index 338f1fe0610a1..9733fa32583f1 100644 --- a/app/code/Magento/CatalogInventory/etc/mview.xml +++ b/app/code/Magento/CatalogInventory/etc/mview.xml @@ -11,6 +11,7 @@ <table name="cataloginventory_stock_item" entity_column="product_id" /> <!--Track product status to trigger stock indexer--> <table name="catalog_product_entity_int" entity_column="entity_id" /> + <table name="catalog_product_link" entity_column="product_id" /> </subscriptions> </view> <view id="catalog_product_price" class="Magento\Catalog\Model\Indexer\Product\Price" group="indexer"> diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Rule.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Rule.php index 662cdede84ede..dd4f3306b8603 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Rule.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Rule.php @@ -187,7 +187,7 @@ public function getRulePrices(\DateTimeInterface $date, $websiteId, $customerGro ->where('rule_date = ?', $date->format('Y-m-d')) ->where('website_id = ?', $websiteId) ->where('customer_group_id = ?', $customerGroupId) - ->where('product_id IN(?)', $productIds); + ->where('product_id IN(?)', $productIds, \Zend_Db::INT_TYPE); return $connection->fetchPairs($select); } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml index fb218297b646d..3c08fbdf641e4 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> - <!-- Perform reindex and flush cache --> - <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + <comment userInput="Adding the comment to replace AdminReindexAndFlushCache action group ('indexer:reindex', 'cache:flush' commands) for preserving Backward Compatibility" stepKey="reindexAndFlushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml index b90cc66a10d68..a40bf63c5a388 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml @@ -38,7 +38,9 @@ <!-- Delete products and category --> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml index 9f793d5c8c393..106e0ffabb2b2 100644 --- a/app/code/Magento/CatalogRule/etc/mview.xml +++ b/app/code/Magento/CatalogRule/etc/mview.xml @@ -21,6 +21,7 @@ <table name="catalog_product_entity_tier_price" entity_column="entity_id" /> <table name="catalog_product_entity_varchar" entity_column="entity_id" /> <table name="catalog_category_product" entity_column="product_id" /> + <table name="catalog_product_link" entity_column="product_id" /> </subscriptions> </view> <view id="catalog_product_price" class="Magento\Catalog\Model\Indexer\Product\Price" group="indexer"> diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml index 48f53da8e2a2e..ca9017b7c5f29 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml +++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml @@ -190,7 +190,7 @@ </actionGroup> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + <comment userInput="Adding the comment to replace AdminReindexAndFlushCache action group ('indexer:reindex', 'cache:flush' commands) for preserving Backward Compatibility" stepKey="reindexAndFlushCache"/> <!-- Login to storefront from customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomerOnStorefront"> diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml index 350f896606c19..b20bd34106e03 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml +++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml @@ -173,7 +173,7 @@ </actionGroup> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplySecondPriceRule"/> - <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + <comment userInput="Adding the comment to replace AdminReindexAndFlushCache action group ('indexer:reindex', 'cache:flush' commands) for preserving Backward Compatibility" stepKey="reindexAndFlushCache"/> <!-- Assert product in storefront product page --> <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php new file mode 100644 index 0000000000000..fbacddac1ce02 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogUrlRewrite\Model\ResourceModel\Product\GetUrlRewriteData; +use Magento\Store\Model\Store; + +/** + * Product data needed for url rewrite generation locator class + */ +class GetProductUrlRewriteDataByStore +{ + /** + * @var array + */ + private $urlRewriteData = []; + + /** + * @var GetUrlRewriteData + */ + private $getUrlRewriteData; + + /** + * @param GetUrlRewriteData $getUrlRewriteData + */ + public function __construct(GetUrlRewriteData $getUrlRewriteData) + { + $this->getUrlRewriteData = $getUrlRewriteData; + } + + /** + * Retrieves data for product by store + * + * @param ProductInterface $product + * @param int $storeId + * @return array + */ + public function execute(ProductInterface $product, int $storeId): array + { + $productId = $product->getId(); + if (isset($this->urlRewriteData[$productId][$storeId])) { + return $this->urlRewriteData[$productId][$storeId]; + } + if (empty($this->urlRewriteData[$productId])) { + $storesData = $this->getUrlRewriteData->execute($product); + foreach ($storesData as $storeData) { + $this->urlRewriteData[$productId][$storeData['store_id']] = [ + 'visibility' => (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']), + 'url_key' => $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'], + ]; + } + } + + if (!isset($this->urlRewriteData[$productId][$storeId])) { + $this->urlRewriteData[$productId][$storeId] = $this->urlRewriteData[$productId][Store::DEFAULT_STORE_ID]; + } + + return $this->urlRewriteData[$productId][$storeId]; + } + + /** + * Clears product url rewrite data in local cache + * + * @param ProductInterface $product + */ + public function clearProductUrlRewriteDataCache(ProductInterface $product) + { + unset($this->urlRewriteData[$product->getId()]); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Products/AppendUrlRewritesToProducts.php b/app/code/Magento/CatalogUrlRewrite/Model/Products/AppendUrlRewritesToProducts.php new file mode 100644 index 0000000000000..15d4aabf4246b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Products/AppendUrlRewritesToProducts.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model\Products; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogUrlRewrite\Model\Product\GetProductUrlRewriteDataByStore; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; +use Magento\Store\Model\Store; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Model\UrlPersistInterface; + +/** + * Update existing url rewrites or create new ones if needed + */ +class AppendUrlRewritesToProducts +{ + /** + * @var ProductUrlRewriteGenerator + */ + private $productUrlRewriteGenerator; + + /** + * @var StoreViewService + */ + private $storeViewService; + + /** + * @var ProductUrlPathGenerator + */ + private $productUrlPathGenerator; + + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var GetProductUrlRewriteDataByStore + */ + private $getDataByStore; + + /** + * @param ProductUrlRewriteGenerator $urlRewriteGenerator + * @param StoreViewService $storeViewService + * @param ProductUrlPathGenerator $urlPathGenerator + * @param UrlPersistInterface $urlPersist + * @param GetProductUrlRewriteDataByStore $getDataByStore + */ + public function __construct( + ProductUrlRewriteGenerator $urlRewriteGenerator, + StoreViewService $storeViewService, + ProductUrlPathGenerator $urlPathGenerator, + UrlPersistInterface $urlPersist, + GetProductUrlRewriteDataByStore $getDataByStore + ) { + $this->productUrlRewriteGenerator = $urlRewriteGenerator; + $this->storeViewService = $storeViewService; + $this->productUrlPathGenerator = $urlPathGenerator; + $this->urlPersist = $urlPersist; + $this->getDataByStore = $getDataByStore; + } + + /** + * Update existing rewrites and add for specific stores websites + * + * @param ProductInterface[] $products + * @param array $storesToAdd + * @throws UrlAlreadyExistsException + */ + public function execute(array $products, array $storesToAdd): void + { + foreach ($products as $product) { + $forceGenerateDefault = false; + foreach ($storesToAdd as $storeId) { + if ($this->needGenerateUrlForStore($product, (int)$storeId)) { + $urls[] = $this->generateUrls($product, (int)$storeId); + } elseif ((int)$product->getStoreId() !== Store::DEFAULT_STORE_ID) { + $forceGenerateDefault = true; + } + } + if ($product->getStoreId() === Store::DEFAULT_STORE_ID + || $this->isProductAssignedToStore($product)) { + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); + $urls[] = $this->productUrlRewriteGenerator->generate($product); + } + if ($forceGenerateDefault && $product->getStoreId() !== Store::DEFAULT_STORE_ID) { + $urls[] = $this->generateUrls($product, Store::DEFAULT_STORE_ID); + } + $this->getDataByStore->clearProductUrlRewriteDataCache($product); + } + if (!empty($urls)) { + $this->urlPersist->replace(array_merge(...$urls)); + } + } + + /** + * Generate urls for specific store + * + * @param ProductInterface $product + * @param int $storeId + * @return array + */ + private function generateUrls(ProductInterface $product, int $storeId): array + { + $storeData = $this->getDataByStore->execute($product, $storeId); + $origStoreId = $product->getStoreId(); + $origVisibility = $product->getVisibility(); + $origUrlKey = $product->getUrlKey(); + $product->setStoreId($storeId); + $product->setVisibility($storeData['visibility'] ?? Visibility::VISIBILITY_NOT_VISIBLE); + $product->setUrlKey($storeData['url_key'] ?? ''); + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); + $urls = $this->productUrlRewriteGenerator->generate($product); + $product->setStoreId($origStoreId); + $product->setVisibility($origVisibility); + $product->setUrlKey($origUrlKey); + + return $urls; + } + + /** + * Does product has scope overridden url key value + * + * @param ProductInterface $product + * @param int $storeId + * @return bool + */ + private function needGenerateUrlForStore(ProductInterface $product, int $storeId): bool + { + return (int)$product->getStoreId() !== $storeId + && $this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( + $storeId, + $product->getId(), + Product::ENTITY + ); + } + + /** + * Is product still assigned to store which request is performed from + * + * @param ProductInterface $product + * @return bool + */ + private function isProductAssignedToStore(ProductInterface $product): bool + { + return in_array($product->getStoreId(), $product->getStoreIds()); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Product/GetUrlRewriteData.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Product/GetUrlRewriteData.php new file mode 100644 index 0000000000000..f4cef73a040e8 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Product/GetUrlRewriteData.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model\ResourceModel\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Fetch product url rewrite data from database + */ +class GetUrlRewriteData +{ + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var Config + */ + private $eavConfig; + + /** + * @param MetadataPool $metadataPool + * @param ResourceConnection $connection + * @param Config $eavConfig + */ + public function __construct( + MetadataPool $metadataPool, + ResourceConnection $connection, + Config $eavConfig + ) { + $this->metadataPool = $metadataPool; + $this->resource = $connection; + $this->eavConfig = $eavConfig; + } + + /** + * Fetches product store data required for url key generation + * + * @param ProductInterface $product + * @return array + */ + public function execute(ProductInterface $product): array + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); + $connection = $this->resource->getConnection(); + $visibilityAttribute = $this->eavConfig->getAttribute(Product::ENTITY, 'visibility'); + $urlKeyAttribute = $this->eavConfig->getAttribute(Product::ENTITY, 'url_key'); + $visibilitySelect = $connection->select() + ->from(['visibility' => $visibilityAttribute->getBackendTable()]) + ->joinRight( + ['url_key' => $urlKeyAttribute->getBackendTable()], + 'url_key.' . $linkField . ' = visibility.' . $linkField . ' AND url_key.store_id = visibility.store_id' + . ' AND url_key.attribute_id = ' . $urlKeyAttribute->getId(), + ['url_key.value as url_key'] + ) + ->reset(Select::COLUMNS) + ->columns(['url_key.store_id', 'url_key.value AS url_key', 'visibility.value AS visibility']) + ->where('visibility.' . $linkField . ' = ?', $product->getData($linkField)) + ->where('visibility.attribute_id = ?', $visibilityAttribute->getId()); + $urlKeySelect = $connection->select() + ->from(['url_key' => $urlKeyAttribute->getBackendTable()]) + ->joinLeft( + ['visibility' => $visibilityAttribute->getBackendTable()], + 'url_key.' . $linkField . ' = visibility.' . $linkField . ' AND url_key.store_id = visibility.store_id' + . ' AND visibility.attribute_id = ' . $visibilityAttribute->getId(), + ['visibility.value as visibility'] + ) + ->reset(Select::COLUMNS) + ->columns(['url_key.store_id', 'url_key.value AS url_key', 'visibility.value as visibility']) + ->where('url_key.' . $linkField . ' = ?', $product->getData($linkField)) + ->where('url_key.attribute_id = ?', $urlKeyAttribute->getId()); + + $select = $connection->select()->union([$visibilitySelect, $urlKeySelect], Select::SQL_UNION); + + return $connection->fetchAll($select); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php index 6eda8dd0b61ee..512340354172e 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php @@ -3,73 +3,156 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Product; -use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogUrlRewrite\Model\Products\AppendUrlRewritesToProducts; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -use Magento\Framework\App\ObjectManager; -use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreResolver\GetStoresListByWebsiteIds; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; /** * Class ProductProcessUrlRewriteSavingObserver + * + * Generates urls for product url rewrites */ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface { /** - * @var ProductUrlRewriteGenerator + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var AppendUrlRewritesToProducts */ - private $productUrlRewriteGenerator; + private $appendRewrites; /** - * @var UrlPersistInterface + * @var ScopeConfigInterface */ - private $urlPersist; + private $scopeConfig; /** - * @var ProductUrlPathGenerator + * @var GetStoresListByWebsiteIds */ - private $productUrlPathGenerator; + private $getStoresList; /** - * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist - * @param ProductUrlPathGenerator|null $productUrlPathGenerator + * @param AppendUrlRewritesToProducts|null $appendRewrites + * @param ScopeConfigInterface $scopeConfig + * @param GetStoresListByWebsiteIds $getStoresList */ public function __construct( - ProductUrlRewriteGenerator $productUrlRewriteGenerator, UrlPersistInterface $urlPersist, - ProductUrlPathGenerator $productUrlPathGenerator = null + AppendUrlRewritesToProducts $appendRewrites, + ScopeConfigInterface $scopeConfig, + GetStoresListByWebsiteIds $getStoresList ) { - $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; - $this->productUrlPathGenerator = $productUrlPathGenerator ?: ObjectManager::getInstance() - ->get(ProductUrlPathGenerator::class); + $this->appendRewrites = $appendRewrites; + $this->scopeConfig = $scopeConfig; + $this->getStoresList = $getStoresList; } /** * Generate urls for UrlRewrite and save it in storage * - * @param \Magento\Framework\Event\Observer $observer + * @param Observer $observer * @return void - * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException + * @throws UrlAlreadyExistsException */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { /** @var Product $product */ $product = $observer->getEvent()->getProduct(); - if ($product->dataHasChangedFor('url_key') - || $product->getIsChangedCategories() - || $product->getIsChangedWebsites() - || $product->dataHasChangedFor('visibility') - ) { - if ($product->isVisibleInSiteVisibility()) { - $product->unsUrlPath(); - $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); - } + if ($this->isNeedUpdateRewrites($product)) { + $this->deleteObsoleteRewrites($product); + $oldWebsiteIds = $product->getOrigData('website_ids') ?? []; + $storesToAdd = $this->getStoresList->execute( + array_diff($product->getWebsiteIds(), $oldWebsiteIds) + ); + $this->appendRewrites->execute([$product], $storesToAdd); } } + + /** + * Remove obsolete Url rewrites + * + * @param Product $product + */ + private function deleteObsoleteRewrites(Product $product): void + { + //do not perform redundant delete request for new product + if ($product->getOrigData('entity_id') === null) { + return; + } + $oldWebsiteIds = $product->getOrigData('website_ids') ?? []; + $storesToRemove = $this->getStoresList->execute( + array_diff($oldWebsiteIds, $product->getWebsiteIds()) + ); + if ((int)$product->getVisibility() === Visibility::VISIBILITY_NOT_VISIBLE) { + $isGlobalScope = $product->getStoreId() == Store::DEFAULT_STORE_ID; + $storesToRemove[] = $isGlobalScope ? $product->getStoreIds() : $product->getStoreId(); + } + if ($storesToRemove) { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storesToRemove, + ] + ); + } + } + + /** + * Is website assignment updated + * + * @param Product $product + * @return bool + */ + private function isWebsiteChanged(Product $product) + { + $oldWebsiteIds = $product->getOrigData('website_ids'); + $newWebsiteIds = $product->getWebsiteIds(); + + return array_diff($oldWebsiteIds, $newWebsiteIds) || array_diff($newWebsiteIds, $oldWebsiteIds); + } + + + /** + * Is product rewrites need to be updated + * + * @param Product $product + * @return bool + */ + private function isNeedUpdateRewrites(Product $product): bool + { + return ($product->dataHasChangedFor('url_key') + && (int)$product->getVisibility() !== Visibility::VISIBILITY_NOT_VISIBLE) + || ($product->getIsChangedCategories() && $this->isGenerateCategoryProductRewritesEnabled()) + || $this->isWebsiteChanged($product) + || $product->dataHasChangedFor('visibility'); + } + + /** + * Return product use category path in rewrite config value + * + * @return bool + */ + private function isGenerateCategoryProductRewritesEnabled(): bool + { + return $this->scopeConfig->isSetFlag('catalog/seo/generate_category_product_rewrites'); + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php deleted file mode 100644 index 44b47faf3d4b8..0000000000000 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\CatalogUrlRewrite\Observer; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Product\Visibility; -use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\Event\ObserverInterface; -use Magento\Store\Model\Store; -use Magento\UrlRewrite\Model\UrlPersistInterface; -use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\Store\Api\StoreWebsiteRelationInterface; -use Magento\Framework\App\ObjectManager; - -/** - * Observer to assign the products to website - */ -class ProductToWebsiteChangeObserver implements ObserverInterface -{ - /** - * @var ProductUrlRewriteGenerator - */ - protected $productUrlRewriteGenerator; - - /** - * @var UrlPersistInterface - */ - protected $urlPersist; - - /** - * @var ProductRepositoryInterface - */ - protected $productRepository; - - /** - * @var RequestInterface - */ - protected $request; - - /** - * @var StoreWebsiteRelationInterface - */ - private $storeWebsiteRelation; - - /** - * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator - * @param UrlPersistInterface $urlPersist - * @param ProductRepositoryInterface $productRepository - * @param RequestInterface $request - * @param StoreWebsiteRelationInterface $storeWebsiteRelation - */ - public function __construct( - ProductUrlRewriteGenerator $productUrlRewriteGenerator, - UrlPersistInterface $urlPersist, - ProductRepositoryInterface $productRepository, - RequestInterface $request, - StoreWebsiteRelationInterface $storeWebsiteRelation = null - ) { - $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; - $this->urlPersist = $urlPersist; - $this->productRepository = $productRepository; - $this->request = $request; - $this->storeWebsiteRelation = $storeWebsiteRelation ?: - ObjectManager::getInstance()->get(StoreWebsiteRelationInterface::class); - } - - /** - * Generate urls for UrlRewrite and save it in storage - * - * @param \Magento\Framework\Event\Observer $observer - * @return void - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - foreach ($observer->getEvent()->getProducts() as $productId) { - $product = $this->productRepository->getById( - $productId, - false, - $this->request->getParam('store_id', Store::DEFAULT_STORE_ID) - ); - - if (!empty($this->productUrlRewriteGenerator->generate($product))) { - if ($this->request->getParam('remove_website_ids')) { - foreach ($this->request->getParam('remove_website_ids') as $webId) { - foreach ($this->storeWebsiteRelation->getStoreByWebsiteId($webId) as $storeId) { - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::STORE_ID => $storeId - ]); - } - } - } - if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); - } - } - } - } -} diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php new file mode 100644 index 0000000000000..f9c605ab489a5 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Plugin\Catalog\Model\Product; + +use Magento\Catalog\Model\Product\Action as ProductAction; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\CatalogUrlRewrite\Model\Products\AppendUrlRewritesToProducts; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Store\Api\StoreWebsiteRelationInterface; +use Magento\Store\Model\StoreResolver\GetStoresListByWebsiteIds; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Update URL rewrites after website change + */ +class UpdateProductWebsiteUrlRewrites +{ + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var Collection + */ + private $productCollection; + + /** + * @var AppendUrlRewritesToProducts + */ + private $appendRewrites; + + /** + * @var GetStoresListByWebsiteIds + */ + private $getStoresList; + + /** + * @param UrlPersistInterface $urlPersist + * @param Collection $productCollection + * @param AppendUrlRewritesToProducts $appendRewrites + * @param GetStoresListByWebsiteIds $getStoresList + */ + public function __construct( + UrlPersistInterface $urlPersist, + Collection $productCollection, + AppendUrlRewritesToProducts $appendRewrites, + GetStoresListByWebsiteIds $getStoresList + ) { + $this->urlPersist = $urlPersist; + $this->productCollection = $productCollection; + $this->appendRewrites = $appendRewrites; + $this->getStoresList = $getStoresList; + } + + /** + * Update url rewrites after website changes + * + * @param ProductAction $subject + * @param void $result + * @param array $productIds + * @param array $websiteIds + * @param string $type + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUpdateWebsites( + ProductAction $subject, + $result, + array $productIds, + array $websiteIds, + string $type + ): void { + if (empty($websiteIds)) { + return; + } + $storeIds = $this->getStoresList->execute($websiteIds); + // Remove the URLs from websites this product no longer belongs to + if ($type == 'remove') { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $productIds, + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storeIds, + ] + ); + } else { + $collection = $this->productCollection->addFieldToFilter('entity_id', ['in' => implode(',', $productIds)]); + $this->appendRewrites->execute($collection->getItems(), $storeIds); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml index d529c6dd3ecc3..fc9eb8529da6f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml @@ -18,7 +18,7 @@ </annotations> <before> <magentoCLI command="config:set {{DisableCreatePermanentRedirect.path}} {{DisableCreatePermanentRedirect.value}}" stepKey="enableCreatePermanentRedirect"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createDefaultCategory"/> @@ -29,7 +29,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> <magentoCLI command="config:set {{EnableCreatePermanentRedirect.path}} {{EnableCreatePermanentRedirect.value}}" stepKey="disableCreatePermanentRedirect"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml index 776b5b9b70f33..783d9fa31c996 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml @@ -31,9 +31,7 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="indexerReindexAfterCreate"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBefore"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php index 30f7608504c23..0ceff2aeff5e5 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php @@ -1,4 +1,5 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -8,11 +9,12 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; use Magento\Catalog\Model\Product; -use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\Products\AppendUrlRewritesToProducts; use Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewriteSavingObserver; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event; use Magento\Framework\Event\Observer; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\StoreResolver\GetStoresListByWebsiteIds; use Magento\UrlRewrite\Model\UrlPersistInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -44,29 +46,39 @@ class ProductProcessUrlRewriteSavingObserverTest extends TestCase protected $product; /** - * @var ProductUrlRewriteGenerator|MockObject + * @var ProductProcessUrlRewriteSavingObserver */ - protected $productUrlRewriteGenerator; + protected $model; /** - * @var ObjectManager + * @var AppendUrlRewritesToProducts|MockObject */ - protected $objectManager; + private $appendRewrites; /** - * @var ProductProcessUrlRewriteSavingObserver + * @var ScopeConfigInterface|MockObject */ - protected $model; + private $scopeConfig; /** - * Set up + * @inheritdoc */ protected function setUp(): void { $this->urlPersist = $this->getMockForAbstractClass(UrlPersistInterface::class); $this->product = $this->getMockBuilder(Product::class) ->addMethods(['getIsChangedWebsites', 'getIsChangedCategories']) - ->onlyMethods(['getId', 'dataHasChangedFor', 'isVisibleInSiteVisibility', 'getStoreId']) + ->onlyMethods( + [ + 'getId', + 'dataHasChangedFor', + 'getVisibility', + 'getStoreId', + 'getWebsiteIds', + 'getOrigData', + 'getCategoryCollection', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->product->expects($this->any())->method('getId')->willReturn(3); @@ -77,20 +89,27 @@ protected function setUp(): void $this->event->expects($this->any())->method('getProduct')->willReturn($this->product); $this->observer = $this->createPartialMock(Observer::class, ['getEvent']); $this->observer->expects($this->any())->method('getEvent')->willReturn($this->event); - $this->productUrlRewriteGenerator = $this->createPartialMock( - ProductUrlRewriteGenerator::class, - ['generate'] - ); - $this->productUrlRewriteGenerator->expects($this->any()) - ->method('generate') - ->willReturn([3 => 'rewrite']); - $this->objectManager = new ObjectManager($this); - $this->model = $this->objectManager->getObject( - ProductProcessUrlRewriteSavingObserver::class, - [ - 'productUrlRewriteGenerator' => $this->productUrlRewriteGenerator, - 'urlPersist' => $this->urlPersist - ] + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->onlyMethods(['isSetFlag']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->appendRewrites = $this->getMockBuilder(AppendUrlRewritesToProducts::class) + ->onlyMethods(['execute']) + ->disableOriginalConstructor() + ->getMock(); + + $getStoresList = $this->getMockBuilder(GetStoresListByWebsiteIds::class) + ->onlyMethods(['execute']) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new ProductProcessUrlRewriteSavingObserver( + $this->urlPersist, + $this->appendRewrites, + $this->scopeConfig, + $getStoresList ); } @@ -103,53 +122,59 @@ public function urlKeyDataProvider() { return [ 'url changed' => [ - 'isChangedUrlKey' => true, - 'isChangedVisibility' => false, - 'isChangedWebsites' => false, - 'isChangedCategories' => false, - 'visibilityResult' => true, - 'expectedReplaceCount' => 1, + 'isChangedUrlKey' => true, + 'isChangedVisibility' => false, + 'isChangedWebsites' => false, + 'isChangedCategories' => false, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 1, + 'websitesWithProduct' => [1], ], 'no chnages' => [ - 'isChangedUrlKey' => false, - 'isChangedVisibility' => false, - 'isChangedWebsites' => false, - 'isChangedCategories' => false, - 'visibilityResult' => true, - 'expectedReplaceCount' => 0 + 'isChangedUrlKey' => false, + 'isChangedVisibility' => false, + 'isChangedWebsites' => false, + 'isChangedCategories' => false, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 0, + 'websitesWithProduct' => [1], ], 'visibility changed' => [ - 'isChangedUrlKey' => false, - 'isChangedVisibility' => true, - 'isChangedWebsites' => false, - 'isChangedCategories' => false, - 'visibilityResult' => true, - 'expectedReplaceCount' => 1 + 'isChangedUrlKey' => false, + 'isChangedVisibility' => true, + 'isChangedWebsites' => false, + 'isChangedCategories' => false, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 1, + 'websitesWithProduct' => [1], ], 'websites changed' => [ - 'isChangedUrlKey' => false, - 'isChangedVisibility' => false, - 'isChangedWebsites' => true, - 'isChangedCategories' => false, - 'visibilityResult' => true, - 'expectedReplaceCount' => 1 + 'isChangedUrlKey' => false, + 'isChangedVisibility' => false, + 'isChangedWebsites' => true, + 'isChangedCategories' => false, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 1, + 'websitesWithProduct' => [1], ], 'categories changed' => [ - 'isChangedUrlKey' => false, - 'isChangedVisibility' => false, - 'isChangedWebsites' => false, - 'isChangedCategories' => true, - 'visibilityResult' => true, - 'expectedReplaceCount' => 1 + 'isChangedUrlKey' => false, + 'isChangedVisibility' => false, + 'isChangedWebsites' => false, + 'isChangedCategories' => true, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 1, + 'websitesWithProduct' => [1], ], 'url changed invisible' => [ - 'isChangedUrlKey' => true, - 'isChangedVisibility' => false, - 'isChangedWebsites' => false, - 'isChangedCategories' => false, - 'visibilityResult' => false, - 'expectedReplaceCount' => 0 + 'isChangedUrlKey' => true, + 'isChangedVisibility' => false, + 'isChangedWebsites' => false, + 'isChangedCategories' => false, + 'visibilityResult' => 1, + 'expectedReplaceCount' => 0, + 'websitesWithProduct' => [1], ], ]; } @@ -161,6 +186,7 @@ public function urlKeyDataProvider() * @param bool $isChangedCategories * @param bool $visibilityResult * @param int $expectedReplaceCount + * @param array $websitesWithProduct * * @dataProvider urlKeyDataProvider */ @@ -170,16 +196,19 @@ public function testExecuteUrlKey( $isChangedWebsites, $isChangedCategories, $visibilityResult, - $expectedReplaceCount + $expectedReplaceCount, + $websitesWithProduct ) { $this->product->expects($this->any())->method('getStoreId')->willReturn(12); $this->product->expects($this->any()) ->method('dataHasChangedFor') - ->willReturnMap([ - ['visibility', $isChangedVisibility], - ['url_key', $isChangedUrlKey] - ]); + ->willReturnMap( + [ + ['visibility', $isChangedVisibility], + ['url_key', $isChangedUrlKey], + ] + ); $this->product->expects($this->any()) ->method('getIsChangedWebsites') @@ -189,13 +218,23 @@ public function testExecuteUrlKey( ->method('getIsChangedCategories') ->willReturn($isChangedCategories); + $this->product->expects($this->any())->method('getWebsiteIds')->will( + $this->returnValue($websitesWithProduct) + ); + $this->product->expects($this->any()) - ->method('isVisibleInSiteVisibility') + ->method('getVisibility') ->willReturn($visibilityResult); - $this->urlPersist->expects($this->exactly($expectedReplaceCount)) - ->method('replace') - ->with([3 => 'rewrite']); + $this->product->expects($this->any()) + ->method('getOrigData') + ->willReturn($isChangedWebsites ? [] : $websitesWithProduct); + $this->scopeConfig->expects($this->any()) + ->method('isSetFlag') + ->willReturn(true); + + $this->appendRewrites->expects($this->exactly($expectedReplaceCount)) + ->method('execute'); $this->model->execute($this->observer); } diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml index 5fb7d33546d60..d22816243f64c 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml @@ -27,6 +27,9 @@ <type name="Magento\CatalogUrlRewrite\Model\Storage\DbStorage"> <plugin name="dynamic_storage_plugin" type="Magento\CatalogUrlRewrite\Plugin\DynamicCategoryRewrites"/> </type> + <type name="Magento\Catalog\Model\Product\Action"> + <plugin name="update_url_rewrites_after_websites_update_plugin" type="Magento\CatalogUrlRewrite\Plugin\Catalog\Model\Product\UpdateProductWebsiteUrlRewrites"/> + </type> <type name="Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder"> <arguments> <argument name="urlRewriteClassNames" xsi:type="array"> diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php index 9429a24199b47..f1cec1c15d861 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php @@ -75,7 +75,7 @@ private function getCategoryUrlSuffix(int $storeId): ?string self::$xml_path_category_url_suffix, ScopeInterface::SCOPE_STORE, $storeId - ); + ) ?? ''; } return $this->categoryUrlSuffix[$storeId]; } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php index 97795db73fcf8..db84784bab5b6 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php @@ -75,7 +75,7 @@ private function getProductUrlSuffix(int $storeId): ?string self::$xml_path_product_url_suffix, ScopeInterface::SCOPE_STORE, $storeId - ); + ) ?? ''; } return $this->productUrlSuffix[$storeId]; } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/CategoryUrlSuffixTest.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/CategoryUrlSuffixTest.php new file mode 100644 index 0000000000000..1d8cb6a391064 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/CategoryUrlSuffixTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogUrlRewriteGraphQl\Test\Unit\Model\Resolver; + +use Magento\CatalogUrlRewriteGraphQl\Model\Resolver\CategoryUrlSuffix; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextExtensionInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Store\Api\Data\StoreInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Test for \Magento\CatalogUrlRewriteGraphQl\Model\Resolver\CategoryUrlSuffix. + */ +class CategoryUrlSuffixTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var ContextInterface|MockObject + */ + private $contextMock; + + /** + * @var ContextExtensionInterface|MockObject + */ + private $contextExtensionMock; + + /** + * @var StoreInterface|MockObject + */ + private $storeMock; + + /** + * @var Field|MockObject + */ + private $fieldMock; + + /** + * @var ResolveInfo|MockObject + */ + private $resolveInfoMock; + + /** + * @var CategoryUrlSuffix + */ + private $resolver; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(ContextInterface::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getExtensionAttributes' + ] + ) + ->getMockForAbstractClass(); + + $this->contextExtensionMock = $this->getMockBuilder(ContextExtensionInterface::class) + ->setMethods( + [ + 'getStore' + ] + ) + ->getMockForAbstractClass(); + + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods( + [ + 'getId' + ] + ) + ->getMockForAbstractClass(); + + $this->fieldMock = $this->getMockBuilder(Field::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resolveInfoMock = $this->getMockBuilder(ResolveInfo::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + + $this->resolver = new CategoryUrlSuffix( + $this->scopeConfigMock + ); + } + + /** + * Verify that empty string is returned when config value is null + */ + public function testNullValue() + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn(null); + + $this->contextMock + ->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->contextExtensionMock); + + $this->contextExtensionMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock + ->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + '', + $this->resolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock + ) + ); + } + + /** + * Verify that the configured value is returned + */ + public function testNonNullValue() + { + $value = 'html'; + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn($value); + + $this->contextMock + ->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->contextExtensionMock); + + $this->contextExtensionMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock + ->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + $value, + $this->resolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock + ) + ); + } +} diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/ProductUrlSuffixTest.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/ProductUrlSuffixTest.php new file mode 100644 index 0000000000000..e133ff8dc5855 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/ProductUrlSuffixTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogUrlRewriteGraphQl\Test\Unit\Model\Resolver; + +use Magento\CatalogUrlRewriteGraphQl\Model\Resolver\ProductUrlSuffix; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextExtensionInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Store\Api\Data\StoreInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Test for \Magento\CatalogUrlRewriteGraphQl\Model\Resolver\ProductUrlSuffix. + */ +class ProductUrlSuffixTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var ContextInterface|MockObject + */ + private $contextMock; + + /** + * @var ContextExtensionInterface|MockObject + */ + private $contextExtensionMock; + + /** + * @var StoreInterface|MockObject + */ + private $storeMock; + + /** + * @var Field|MockObject + */ + private $fieldMock; + + /** + * @var ResolveInfo|MockObject + */ + private $resolveInfoMock; + + /** + * @var ProductUrlSuffix + */ + private $resolver; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(ContextInterface::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getExtensionAttributes' + ] + ) + ->getMockForAbstractClass(); + + $this->contextExtensionMock = $this->getMockBuilder(ContextExtensionInterface::class) + ->setMethods( + [ + 'getStore' + ] + ) + ->getMockForAbstractClass(); + + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods( + [ + 'getId' + ] + ) + ->getMockForAbstractClass(); + + $this->fieldMock = $this->getMockBuilder(Field::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resolveInfoMock = $this->getMockBuilder(ResolveInfo::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + + $this->resolver = new ProductUrlSuffix( + $this->scopeConfigMock + ); + } + + /** + * Verify that empty string is returned when config value is null + */ + public function testNullValue() + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn(null); + + $this->contextMock + ->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->contextExtensionMock); + + $this->contextExtensionMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock + ->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + '', + $this->resolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock + ) + ); + } + + /** + * Verify that the configured value is returned + */ + public function testNonNullValue() + { + $value = 'html'; + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn($value); + + $this->contextMock + ->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->contextExtensionMock); + + $this->contextExtensionMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock + ->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + $value, + $this->resolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock + ) + ); + } +} diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckSelectedShippingAddressInCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckSelectedShippingAddressInCheckoutActionGroup.xml index 0c952af6d53fa..ab588bd49436f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckSelectedShippingAddressInCheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckSelectedShippingAddressInCheckoutActionGroup.xml @@ -16,13 +16,12 @@ <argument name="customerVar"/> <argument name="customerAddressVar"/> </arguments> - <waitForElement selector="{{CheckoutShippingSection.shippingTab}}" time="30" stepKey="waitForShippingSectionLoaded"/> - <see stepKey="VerifyFirstNameInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.firstname}}"/> - <see stepKey="VerifyLastNameInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.lastname}}"/> - <see stepKey="VerifyStreetInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.street[0]}}"/> - <see stepKey="VerifyCityInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.city}}"/> - <see stepKey="VerifyZipInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.postcode}}"/> - <see stepKey="VerifyPhoneInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.telephone}}"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.firstname}}" stepKey="VerifyFirstNameInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.lastname}}" stepKey="VerifyLastNameInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.street[0]}}" stepKey="VerifyStreetInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.city}}" stepKey="VerifyCityInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.postcode}}" stepKey="VerifyZipInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.telephone}}" stepKey="VerifyPhoneInSelectedAddress"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup.xml new file mode 100644 index 0000000000000..8b1d9665fbdf8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup"> + <annotations> + <description>Validates that the provided Customer and Address details are listed on the Storefront Checkout page under the 'Billing Address' section when multiple Addresses are present for a Customer.</description> + </annotations> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <waitForElement selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" time="30" stepKey="waitForBillingSectionLoaded"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerVar.firstname}}" stepKey="verifyFirstNameInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerVar.lastname}}" stepKey="verifyLastNameInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerAddressVar.street[0]}}" stepKey="verifyStreetInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerAddressVar.city}}" stepKey="verifyCityInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerAddressVar.postcode}}" stepKey="verifyZipInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerAddressVar.telephone}}" stepKey="verifyPhoneInSelectedAddress"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickSaveAddressButtonActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickSaveAddressButtonActionGroup.xml index a1e7497aa9d43..3e72f5e78a101 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickSaveAddressButtonActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickSaveAddressButtonActionGroup.xml @@ -12,8 +12,9 @@ <annotations> <description>Click Save Address button on checkout.</description> </annotations> - - <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveAddress"/> + <waitForElementVisible selector="{{CheckoutShippingSection.saveAddress}}" stepKey="waitForSaveButton"/> + <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="clickSaveButton"/> <waitForPageLoad stepKey="waitForAddressSaved"/> + <waitForElementNotVisible selector="{{CheckoutShippingSection.newAddressForm}}" stepKey="waitForNewAddressFormGone"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickAddNewAddressButtonFromCheckoutShippingActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickAddNewAddressButtonFromCheckoutShippingActionGroup.xml new file mode 100644 index 0000000000000..34a120d111d0b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickAddNewAddressButtonFromCheckoutShippingActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickAddNewAddressButtonFromCheckoutShippingActionGroup"> + <annotations> + <description>Clicks the New Address button on the storefront Checkout Shipping page</description> + </annotations> + <waitForElementVisible selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="waitForAddNewAddressButton"/> + <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="clickAddNewAddressButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <waitForElementVisible selector="{{CheckoutShippingSection.newAddressForm}}" stepKey="waitForNewAddressForm"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickUpdateAddressInCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickUpdateAddressInCheckoutActionGroup.xml new file mode 100644 index 0000000000000..e83fc452b6962 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickUpdateAddressInCheckoutActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickUpdateAddressInCheckoutActionGroup"> + <annotations> + <description>Clicks the Update button on the checkout page when entering a New Address.</description> + </annotations> + <waitForElementVisible selector="{{CheckoutShippingSection.updateAddress}}" stepKey="waitForUpdateButton"/> + <click selector="{{CheckoutShippingSection.updateAddress}}" stepKey="clickUpdateButton"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectAddressInCheckoutAddressDropDownActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectAddressInCheckoutAddressDropDownActionGroup.xml new file mode 100644 index 0000000000000..39338d490bc35 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectAddressInCheckoutAddressDropDownActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectAddressInCheckoutAddressDropDownActionGroup"> + <annotations> + <description>Selects the specified option in the address selection drop down on the storefront Checkout page.</description> + </annotations> + <arguments> + <argument name="address" defaultValue="New Address" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutPaymentSection.addressDropdown}}" stepKey="waitForAddressDropDownToBeVisible"/> + <selectOption selector="{{CheckoutPaymentSection.addressDropdown}}" userInput="{{address}}" stepKey="selectAddressOption"/> + <waitForPageLoad stepKey="waitForAddressLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup.xml new file mode 100644 index 0000000000000..574ede3fe54d1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup"> + <annotations> + <description>Selects the specified address after 'Change Address' pop up has been opened on the Storefront Checkout page on the 'Payment' step.</description> + </annotations> + <arguments> + <argument name="address" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutBillingAddressSearchSection.selectButton(address)}}" stepKey="waitForAddress"/> + <click selector="{{CheckoutBillingAddressSearchSection.selectButton(address)}}" stepKey="clickSelectForAddress"/> + <waitForElementNotVisible selector="{{CheckoutShippingAddressSearchSection.popupSelectShippingAddress}}" stepKey="waitForPopupClosed"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index 59d46e8cca696..a5eba8835e354 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -15,6 +15,7 @@ <element name="editAddressButton" type="button" selector=".action-edit-address" timeout="30"/> <element name="addressDropdown" type="select" selector="[name=billing_address_id]"/> <element name="newAddressButton" type="button" selector=".action-show-popup" timeout="30"/> + <element name="newAddressForm" type="text" selector="#co-shipping-form"/> <element name="email" type="input" selector="input[id*=customer-email]"/> <element name="password" type="input" selector="#customer-password"/> <element name="firstName" type="input" selector="input[name=firstname]"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index df229c4b6ed78..ffbd6152af80b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -58,6 +58,11 @@ <argument name="address" value="US_Address_TX"/> </actionGroup> + <!-- Select Free Shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="setShippingMethodFreeShipping"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <!-- Assert Free Shipping checkbox --> <seeCheckboxIsChecked selector="{{CheckoutShippingMethodsSection.shippingMethodFreeShipping}}" stepKey="freeShippingMethodCheckboxIsChecked"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml index 1b9831e7dd0bd..d46ebfd63203a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -31,11 +31,12 @@ </actionGroup> </before> <after> + <!--Logout from customer account--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <!--Logout from customer account--> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <comment userInput="BIC workaround" stepKey="logoutCustomer"/> </after> <!-- Add simple product to cart and go to checkout--> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml index ce8bfc37389fb..38e2203b45258 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml @@ -107,13 +107,14 @@ <deleteData createDataKey="createDownloadableProduct" stepKey="deleteDownloadableProduct"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> - - <!-- Delete customer --> - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <comment userInput="BIC workaround" stepKey="deleteCustomer"/> <!-- Logout customer --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCreatedCustomer"/> + <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml index f1ce2f25e6d60..45f1df1237d09 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -21,7 +21,6 @@ <issueId value="DEPRECATED">Use StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest instead</issueId> </skip> </annotations> - <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> @@ -64,7 +63,6 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartRule"> <argument name="product" value="$$simpleProduct$$"/> <argument name="couponCode" value="{{CatPriceRule.coupon_code}}"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml index 3c090900563a5..714a06510952f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml @@ -94,7 +94,7 @@ <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="selectViewAndEditCart"/> <!--Assert Shopping Cart Summary--> - <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="AssertCartSummary" > <argument name="subtotal" value="$100.00"/> <argument name="shipping" value="10.00"/> <argument name="total" value="110.00"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml index eff18f9081b67..cd6f4215adb5d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml @@ -88,7 +88,7 @@ <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="selectViewAndEditCart"/> <!--Assert Shopping Cart Summary--> - <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="AssertCartSummary" > <argument name="subtotal" value="$50.00"/> <argument name="shipping" value="5.00"/> <argument name="total" value="55.00"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml index e97f7f0d3e8e4..535a3fb0c436b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml @@ -28,12 +28,13 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteSimpleCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteUsCustomer"/> <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="resetCustomerFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <comment userInput="BIC workaround" stepKey="logoutCustomer"/> </after> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="storefrontCustomerLogin"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html index 23bbce48fee2c..6b3de69d1a21d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html @@ -7,7 +7,7 @@ <div if="isAddressDetailsVisible() && currentBillingAddress()" class="billing-address-details"> <text args="currentBillingAddress().prefix"/> <text args="currentBillingAddress().firstname"/> <text args="currentBillingAddress().middlename"/> <text args="currentBillingAddress().lastname"/> <text args="currentBillingAddress().suffix"/><br/> - <text args="_.values(currentBillingAddress().street).join(', ')"/><br/> + <text args="currentBillingAddress().street.join(', ')"/><br/> <text args="currentBillingAddress().city "/>, <span text="currentBillingAddress().region"></span> <text args="currentBillingAddress().postcode"/><br/> <text args="getCountryName(currentBillingAddress().countryId)"/><br/> <a if="currentBillingAddress().telephone" attr="'href': 'tel:' + currentBillingAddress().telephone" text="currentBillingAddress().telephone"></a><br/> diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php index 39fcdf86fdcf4..29c4812cc7b96 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php @@ -101,7 +101,7 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f $select->columns(['status' => $stockStatusExpr]); if ($entityIds !== null) { - $select->where('e.entity_id IN(?)', $entityIds); + $select->where('e.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } return $select; diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php index 9d779d9704c29..0dd38062e5a65 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php @@ -203,7 +203,7 @@ public function getParentIdsByChild($childId) ['e' => $this->getTable('catalog_product_entity')], 'e.' . $this->optionProvider->getProductEntityLinkField() . ' = l.parent_id', ['e.entity_id'] - )->where('l.product_id IN(?)', $childId); + )->where('l.product_id IN(?)', $childId, \Zend_Db::INT_TYPE); $parentIds = $this->getConnection()->fetchCol($select); return $parentIds; diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml new file mode 100644 index 0000000000000..0b9b5c98d9884 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckProductQtyAfterOrderCancellingTest"> + + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Cancel order"/> + <title value="Product quantity return after order cancel"/> + <description value="Check Product quantity return after order cancel"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97228"/> + <useCaseId value="MAGETWO-82221"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <createData entity="defaultSimpleProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="GuestCart" stepKey="createGuestCart"/> + <createData entity="FourCartItems" stepKey="addCartItem"> + <requiredEntity createDataKey="createGuestCart"/> + <requiredEntity createDataKey="createConfigProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddress"> + <requiredEntity createDataKey="createGuestCart"/> + </createData> + <updateData createDataKey="createGuestCart" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformation"> + <requiredEntity createDataKey="createGuestCart"/> + </updateData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + </after> + + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> + <argument name="orderId" value="$createGuestCart.return$"/> + </actionGroup> + + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="openOrder"/> + + <actionGroup ref="AdminInvoiceWithUpdatedProductQtyActionGroup" stepKey="createPartialInvoice"> + <argument name="qty" value="1"/> + </actionGroup> + + <actionGroup ref="AdminCreateShipmentFromOrderPage" stepKey="createShipment"> + <argument name="Qty" value="1"/> + <argument name="Number" value="111"/> + </actionGroup> + + <actionGroup ref="CancelPendingOrderActionGroup" stepKey="cancelOrder"> + <argument name="orderStatus" value="Complete"/> + </actionGroup> + + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Canceled 3" stepKey="seeCanceledQty"/> + + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> + + <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createConfigProduct.sku$$"/> + </actionGroup> + + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="assertProductDataInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Quantity"/> + <argument name="value" value="99"/> + </actionGroup> + + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml index f8cd1760788a8..4915a17c738a4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -91,9 +91,7 @@ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="seeConfigurationsPrice"/> <!--Run re-index task--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Assert storefront category list page--> <amOnPage url="/" stepKey="amOnStorefront"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml index 120734d679d09..4984d296df5d0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -131,10 +131,7 @@ <!-- Save product --> <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveProduct"/> - <!--Run re-index task--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!-- Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index e34bf7c22f06b..1b8dffbd3ac68 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -78,9 +78,7 @@ <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexAll"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml index 6e977a7749145..e0dae94f13150 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml @@ -121,7 +121,11 @@ <argument name="productId" value="$$createConfigChildProduct2.id$$"/> </actionGroup> <waitForPageLoad stepKey="waitForSecondChildProductPageLoad"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="outOfStockStatus"/> + + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="outOfStockStatus"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondProductForm"/> <!-- Go to created customer page --> <comment userInput="Go to created customer page" stepKey="goToCreatedCustomerPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml index 0b7bca201ec32..4baceead08a07 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml @@ -8,17 +8,20 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ProductsQtyReturnAfterOrderCancelTest"> + <test name="ProductsQtyReturnAfterOrderCancelTest" deprecated="Use AdminCheckProductQtyAfterOrderCancellingTest instead"> <annotations> <features value="ConfigurableProduct"/> <stories value="Cancel order"/> - <title value="Product quantity return after order cancel"/> + <title value="DEPRECATED. Product quantity return after order cancel"/> <description value="Check Product quantity return after order cancel"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-97228"/> <useCaseId value="MAGETWO-82221"/> <group value="ConfigurableProduct"/> + <skip> + <issueId value="DEPRECATED">Use AdminCheckProductQtyAfterOrderCancellingTest instead</issueId> + </skip> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml index 4ad2d0dc936eb..ca3065c13ea67 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -144,9 +144,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexAll"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml index e20a6dcfa09b8..81a083c5da068 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml @@ -27,9 +27,7 @@ <argument name="category" value="$$createCategory$$"/> </actionGroup> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value="eav"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml index 3519503c1e287..65ba89d5efb1f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml @@ -112,9 +112,7 @@ <argument name="categoryName" value="$$secondCategory.name$$"/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexSearchIndex"> - <argument name="indices" value="catalogsearch_fulltext"/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexSearchIndex"/> <!-- Go to storefront to view child product --> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToSecondCategoryStorefront"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml index 363a8ea4d4fd6..9cbe134116e51 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -153,9 +153,7 @@ <argument name="discountAmount" value="{{CatalogRuleByPercentWith96Amount.discount_amount}}"/> </actionGroup> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexIndices"> - <argument name="indices" value="catalogsearch_fulltext catalog_category_product catalog_product_price catalogrule_rule"/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexIndices"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="fullCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml index 9b046d5c71cfc..ea309271abace 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -132,14 +132,13 @@ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="selectFirstRow"/> <waitForPageLoad stepKey="waitForProductPageToLoad"/> <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="disableProduct"/> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="disableProduct"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> - <!--Run re-index task--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Open Category in Store Front and select product attribute option from sidebar --> <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 79705e679fb78..14ce1fe71281b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -120,7 +120,9 @@ <actionGroup ref="AdminFormSaveAndDuplicateActionGroup" stepKey="saveAndDuplicateProductForm"/> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickEnableProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createConfigProduct.name$$-Updated" stepKey="fillProductName"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="1" stepKey="selectInStock"/> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="selectInStock"> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> <!--Change product image--> <comment userInput="Change product image" stepKey="commentChangeProductImage"/> <actionGroup ref="RemoveProductImageActionGroup" stepKey="removeProductImage"/> @@ -192,9 +194,7 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateConfigsForDuplicatedProduct"/> <waitForPageLoad stepKey="waitForDuplicatedProductPageLoad"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveDuplicatedProduct"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Cron/Model/DeadlockRetrier.php b/app/code/Magento/Cron/Model/DeadlockRetrier.php index 15497910a089b..ab180e93e0ca3 100644 --- a/app/code/Magento/Cron/Model/DeadlockRetrier.php +++ b/app/code/Magento/Cron/Model/DeadlockRetrier.php @@ -17,6 +17,20 @@ */ class DeadlockRetrier implements DeadlockRetrierInterface { + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @param \Psr\Log\LoggerInterface $logger + */ + public function __construct( + \Psr\Log\LoggerInterface $logger + ) { + $this->logger = $logger; + } + /** * @inheritdoc */ @@ -30,6 +44,7 @@ public function execute(callable $callback, AdapterInterface $connection) try { return $callback(); } catch (DeadlockException $e) { + $this->logger->warning(sprintf("Deadlock detected in cron: %s", $e->getMessage())); continue; } } diff --git a/app/code/Magento/Cron/Model/ResourceModel/Schedule.php b/app/code/Magento/Cron/Model/ResourceModel/Schedule.php index 25ebaec5582c9..120e0ce6432c5 100644 --- a/app/code/Magento/Cron/Model/ResourceModel/Schedule.php +++ b/app/code/Magento/Cron/Model/ResourceModel/Schedule.php @@ -65,31 +65,47 @@ public function trySetJobStatusAtomic($scheduleId, $newStatus, $currentStatus) public function trySetJobUniqueStatusAtomic($scheduleId, $newStatus, $currentStatus) { $connection = $this->getConnection(); + $connection->beginTransaction(); // this condition added to avoid cron jobs locking after incorrect termination of running job $match = $connection->quoteInto( 'existing.job_code = current.job_code ' . - 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL) ' . - 'AND existing.status = ?', + 'AND existing.status = ? ' . + 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL)', $newStatus ); + // Select and lock all related schedules - this prevents deadlock in case cron overlaps and two jobs of + // the same code attempt to lock at the same time, and force them to serialize $selectIfUnlocked = $connection->select() + ->from( + ['current' => $this->getTable('cron_schedule')], + [] + ) ->joinLeft( ['existing' => $this->getTable('cron_schedule')], $match, - ['status' => new \Zend_Db_Expr($connection->quote($newStatus))] + ['existing.schedule_id'] ) ->where('current.schedule_id = ?', $scheduleId) ->where('current.status = ?', $currentStatus) - ->where('existing.schedule_id IS NULL'); - - $update = $connection->updateFromSelect($selectIfUnlocked, ['current' => $this->getTable('cron_schedule')]); - $result = $connection->query($update)->rowCount(); + ->forUpdate(true); - if ($result == 1) { - return true; + $scheduleId = $connection->fetchOne($selectIfUnlocked); + if (!empty($scheduleId)) { + // Existing running schedule found + $connection->commit(); + return false; } - return false; + + // Mark our schedule as running + $connection->update( + $this->getTable('cron_schedule'), + ['status' => new \Zend_Db_Expr($connection->quote($newStatus))], + ['schedule_id = ?' => $scheduleId] + ); + + $connection->commit(); + return true; } } diff --git a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php index acffba02eb461..0f266b5d62d83 100644 --- a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php +++ b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php @@ -9,6 +9,7 @@ */ namespace Magento\Cron\Observer; +use Magento\Cron\Model\ResourceModel\Schedule\Collection as ScheduleCollection; use Magento\Cron\Model\Schedule; use Magento\Framework\App\State; use Magento\Framework\Console\Cli; @@ -83,7 +84,7 @@ class ProcessCronQueueObserver implements ObserverInterface const MAX_RETRIES = 5; /** - * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection + * @var ScheduleCollection */ protected $_pendingSchedules; @@ -278,12 +279,12 @@ function ($groupId) use ($currentTime) { * * It should be taken by standalone (child) process, not by the parent process. * - * @param int $groupId + * @param string $groupId * @param callable $callback * * @return void */ - private function lockGroup($groupId, callable $callback) + private function lockGroup(string $groupId, callable $callback): void { if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { $this->logger->warning( @@ -399,7 +400,7 @@ function () use ($schedule) { * @param string $jobName * @return void */ - private function startProfiling(string $jobName = '') + private function startProfiling(string $jobName = ''): void { $this->statProfiler->clear(); $this->statProfiler->start( @@ -416,7 +417,7 @@ private function startProfiling(string $jobName = '') * @param string $jobName * @return void */ - private function stopProfiling(string $jobName = '') + private function stopProfiling(string $jobName = ''): void { $this->statProfiler->stop( sprintf(self::CRON_TIMERID, $jobName), @@ -445,9 +446,9 @@ private function getProfilingStat(string $jobName): string * Return job collection from data base with status 'pending'. * * @param string $groupId - * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + * @return ScheduleCollection */ - private function getPendingSchedules($groupId) + private function getPendingSchedules(string $groupId): ScheduleCollection { $jobs = $this->_config->getJobs(); $pendingJobs = $this->_scheduleFactory->create()->getCollection(); @@ -462,7 +463,7 @@ private function getPendingSchedules($groupId) * @param string $groupId * @return $this */ - private function generateSchedules($groupId) + private function generateSchedules(string $groupId): self { /** * check if schedule generation is needed @@ -533,13 +534,13 @@ protected function _generateJobs($jobs, $exists, $groupId) * @param int $currentTime * @return void */ - private function cleanupJobs($groupId, $currentTime) + private function cleanupJobs(string $groupId, int $currentTime): void { // check if history cleanup is needed $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { - return $this; + return; } // save time history cleanup was ran with no expiration $this->_cache->save( @@ -550,6 +551,7 @@ private function cleanupJobs($groupId, $currentTime) ); $this->cleanupDisabledJobs($groupId); + $this->cleanupRunningJobs($groupId); $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); @@ -673,7 +675,7 @@ protected function getScheduleTimeInterval($groupId) * @param string $groupId * @return void */ - private function cleanupDisabledJobs($groupId) + private function cleanupDisabledJobs(string $groupId): void { $jobs = $this->_config->getJobs(); $jobsToCleanup = []; @@ -696,6 +698,33 @@ private function cleanupDisabledJobs($groupId) } } + /** + * Cleanup jobs that were left in a running state due to an unexpected stop + * + * @param string $groupId + * @return void + */ + private function cleanupRunningJobs(string $groupId): void + { + $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $connection = $scheduleResource->getConnection(); + + $jobs = $this->_config->getJobs(); + + $connection->update( + $scheduleResource->getTable('cron_schedule'), + [ + 'status' => \Magento\Cron\Model\Schedule::STATUS_ERROR, + 'messages' => 'Time out' + ], + [ + $connection->quoteInto('status = ?', \Magento\Cron\Model\Schedule::STATUS_RUNNING), + $connection->quoteInto('job_code IN (?)', array_keys($jobs[$groupId])), + 'scheduled_at < UTC_TIMESTAMP() - INTERVAL 1 DAY' + ] + ); + } + /** * Get cron expression of cron job. * @@ -773,13 +802,13 @@ private function isGroupInFilter($groupId): bool * @param array $jobsRoot * @param int $currentTime */ - private function processPendingJobs($groupId, $jobsRoot, $currentTime) + private function processPendingJobs(string $groupId, array $jobsRoot, int $currentTime): void { - $procesedJobs = []; + $processedJobs = []; $pendingJobs = $this->getPendingSchedules($groupId); /** @var Schedule $schedule */ foreach ($pendingJobs as $schedule) { - if (isset($procesedJobs[$schedule->getJobCode()])) { + if (isset($processedJobs[$schedule->getJobCode()])) { // process only on job per run continue; } @@ -796,7 +825,7 @@ private function processPendingJobs($groupId, $jobsRoot, $currentTime) $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { - $procesedJobs[$schedule->getJobCode()] = true; + $processedJobs[$schedule->getJobCode()] = true; } $this->retrier->execute( @@ -821,7 +850,7 @@ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, { // use sha1 to limit length // phpcs:ignore Magento2.Security.InsecureFunction - $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); + $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); try { for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { diff --git a/app/code/Magento/Cron/Test/Unit/Model/DeadlockRetrierTest.php b/app/code/Magento/Cron/Test/Unit/Model/DeadlockRetrierTest.php index 60eaa091a761f..36e4537383aa6 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/DeadlockRetrierTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/DeadlockRetrierTest.php @@ -13,6 +13,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\DeadlockException; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; class DeadlockRetrierTest extends \PHPUnit\Framework\TestCase { @@ -27,6 +28,11 @@ class DeadlockRetrierTest extends \PHPUnit\Framework\TestCase */ private $adapterMock; + /** + * @var LoggerInterface|MockObject + */ + private $loggerMock; + /** * @var AbstractModel|MockObject */ @@ -38,8 +44,9 @@ class DeadlockRetrierTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { $this->adapterMock = $this->getMockForAbstractClass(AdapterInterface::class); + $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); $this->modelMock = $this->createMock(AbstractModel::class); - $this->retrier = new DeadlockRetrier(); + $this->retrier = new DeadlockRetrier($this->loggerMock); } /** @@ -75,6 +82,8 @@ public function testRetry(): void $this->modelMock->expects($this->exactly(DeadlockRetrierInterface::MAX_RETRIES)) ->method('getId') ->willThrowException(new DeadlockException()); + $this->loggerMock->expects($this->exactly(DeadlockRetrierInterface::MAX_RETRIES - 1)) + ->method('warning'); $this->retrier->execute( function () { @@ -95,6 +104,8 @@ public function testRetrySecond(): void $this->modelMock->expects($this->at(1)) ->method('getId') ->willReturn(2); + $this->loggerMock->expects($this->once()) + ->method('warning'); $this->retrier->execute( function () { diff --git a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php index a48a3dd76b884..50e1d828d2720 100644 --- a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php +++ b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php @@ -773,15 +773,10 @@ function ($callback) { ->setMethods(['execute'])->getMock(); $testCronJob->expects($this->atLeastOnce())->method('execute')->with($schedule); - $this->objectManagerMock->expects( - $this->once() - )->method( - 'create' - )->with( - 'CronJob' - )->willReturn( - $testCronJob - ); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with('CronJob') + ->willReturn($testCronJob); $this->cronQueueObserver->execute($this->observerMock); } @@ -1049,9 +1044,36 @@ public function testMissedJobsCleanedInTime() $this->scheduleCollectionMock->expects($this->any())->method('load')->willReturnSelf(); $scheduleMock->expects($this->any())->method('getCollection')->willReturn($this->scheduleCollectionMock); - $scheduleMock->expects($this->exactly(9))->method('getResource')->willReturn($this->scheduleResourceMock); - $this->scheduleFactoryMock->expects($this->exactly(10))->method('create')->willReturn($scheduleMock); + $scheduleMock->expects($this->exactly(10))->method('getResource')->willReturn($this->scheduleResourceMock); + $this->scheduleFactoryMock->expects($this->exactly(11))->method('create')->willReturn($scheduleMock); + + $connectionMock = $this->prepareConnectionMock($tableName); + + $this->scheduleResourceMock->expects($this->exactly(6)) + ->method('getTable') + ->with($tableName) + ->willReturn($tableName); + $this->scheduleResourceMock->expects($this->exactly(15)) + ->method('getConnection') + ->willReturn($connectionMock); + + $this->retrierMock->expects($this->exactly(5)) + ->method('execute') + ->willReturnCallback( + function ($callback) { + return $callback(); + } + ); + + $this->cronQueueObserver->execute($this->observerMock); + } + /** + * @param string $tableName + * @return AdapterInterface|MockObject + */ + private function prepareConnectionMock(string $tableName) + { $connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); $connectionMock->expects($this->exactly(5)) @@ -1080,22 +1102,30 @@ public function testMissedJobsCleanedInTime() ) ->willReturn(1); - $this->scheduleResourceMock->expects($this->exactly(5)) - ->method('getTable') - ->with($tableName) - ->willReturn($tableName); - $this->scheduleResourceMock->expects($this->exactly(14)) - ->method('getConnection') - ->willReturn($connectionMock); - - $this->retrierMock->expects($this->exactly(5)) - ->method('execute') - ->willReturnCallback( - function ($callback) { - return $callback(); - } + $connectionMock->expects($this->any()) + ->method('quoteInto') + ->withConsecutive( + ['status = ?', \Magento\Cron\Model\Schedule::STATUS_RUNNING], + ['job_code IN (?)', ['test_job1']], + ) + ->willReturnOnConsecutiveCalls( + "status = 'running'", + "job_code IN ('test_job1')" ); - $this->cronQueueObserver->execute($this->observerMock); + $connectionMock->expects($this->once()) + ->method('update') + ->with( + $tableName, + ['status' => 'error', 'messages' => 'Time out'], + [ + "status = 'running'", + "job_code IN ('test_job1')", + 'scheduled_at < UTC_TIMESTAMP() - INTERVAL 1 DAY' + ] + ) + ->willReturn(0); + + return $connectionMock; } } diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml index 609b435f8b39c..72b1428756898 100644 --- a/app/code/Magento/Cron/etc/db_schema.xml +++ b/app/code/Magento/Cron/etc/db_schema.xml @@ -21,16 +21,10 @@ <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="schedule_id"/> </constraint> - <index referenceId="CRON_SCHEDULE_JOB_CODE" indexType="btree"> + <index referenceId="CRON_SCHEDULE_JOB_CODE_STATUS_SCHEDULED_AT" indexType="btree"> <column name="job_code"/> - </index> - <index referenceId="CRON_SCHEDULE_SCHEDULED_AT_STATUS" indexType="btree"> - <column name="scheduled_at"/> - <column name="status"/> - </index> - <index referenceId="CRON_SCHEDULE_SCHEDULE_ID_STATUS" indexType="btree"> - <column name="schedule_id"/> <column name="status"/> + <column name="scheduled_at"/> </index> </table> </schema> diff --git a/app/code/Magento/Cron/etc/db_schema_whitelist.json b/app/code/Magento/Cron/etc/db_schema_whitelist.json index f0d6ebed8290f..2e5cc6e0a4618 100644 --- a/app/code/Magento/Cron/etc/db_schema_whitelist.json +++ b/app/code/Magento/Cron/etc/db_schema_whitelist.json @@ -13,7 +13,8 @@ "index": { "CRON_SCHEDULE_JOB_CODE": true, "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true, - "CRON_SCHEDULE_SCHEDULE_ID_STATUS": true + "CRON_SCHEDULE_SCHEDULE_ID_STATUS": true, + "CRON_SCHEDULE_JOB_CODE_STATUS_SCHEDULED_AT": true }, "constraint": { "PRIMARY": true diff --git a/app/code/Magento/Customer/Model/Group/Resolver.php b/app/code/Magento/Customer/Model/Group/Resolver.php new file mode 100644 index 0000000000000..fd797d744e6dc --- /dev/null +++ b/app/code/Magento/Customer/Model/Group/Resolver.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Group; + +use Magento\Customer\Model\ResourceModel\Group\Resolver as ResolverResource; + +/** + * Lightweight service for getting current customer group based on customer id + */ +class Resolver +{ + /** + * @var ResolverResource + */ + private $resolverResource; + + /** + * @param ResolverResource $resolverResource + */ + public function __construct(ResolverResource $resolverResource) + { + $this->resolverResource = $resolverResource; + } + + /** + * Return customer group id + * + * @param int $customerId + * @return int|null + */ + public function resolve(int $customerId) : ?int + { + return $this->resolverResource->resolve($customerId); + } +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group/Resolver.php b/app/code/Magento/Customer/Model/ResourceModel/Group/Resolver.php new file mode 100644 index 0000000000000..82c2cf2449cd3 --- /dev/null +++ b/app/code/Magento/Customer/Model/ResourceModel/Group/Resolver.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\ResourceModel\Group; + +use Magento\Framework\App\ResourceConnection; + +/** + * Resource model for customer group resolver service + */ +class Resolver +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Resolve customer group from db + * + * @param int $customerId + * @return int|null + */ + public function resolve(int $customerId) : ?int + { + $result = null; + + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('customer_entity'); + + $query = $connection + ->select() + ->from( + ['main_table' => $tableName], + ['main_table.group_id'] + ) + ->where('main_table.entity_id = ?', $customerId); + $groupId = $connection->fetchOne($query); + if ($groupId) { + $result = (int) $groupId; + } + + return $result; + } +} diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenAddressesTabFromCustomerEditPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenAddressesTabFromCustomerEditPageActionGroup.xml new file mode 100644 index 0000000000000..5c9d314ca728a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenAddressesTabFromCustomerEditPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenAddressesTabFromCustomerEditPageActionGroup"> + <annotations> + <description>Open the Addresses tab from a Customer's edit page in admin</description> + </annotations> + <waitForElementVisible selector="{{AdminCustomerInformationSection.addresses}}" stepKey="waitForAddressesTab"/> + <click selector="{{AdminCustomerInformationSection.addresses}}" stepKey="clickAddressesTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" stepKey="waitForCustomerAddressesGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup.xml new file mode 100644 index 0000000000000..bad564c6c9387 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup"> + <annotations> + <description>Update customer website in customer information page</description> + </annotations> + <arguments> + <argument name="websiteName" defaultValue="{{_defaultWebsite.name}}" type="string"/> + </arguments> + <selectOption selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{websiteName}}" stepKey="changeWebsite"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SelectDropdownCustomerAddressAttributeValueActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SelectDropdownCustomerAddressAttributeValueActionGroup.xml index 5d0fb2e7b5c8d..659f0ea5b2640 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SelectDropdownCustomerAddressAttributeValueActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SelectDropdownCustomerAddressAttributeValueActionGroup.xml @@ -16,7 +16,7 @@ <argument name="customerAddressAttribute"/> <argument name="optionValue" type="string"/> </arguments> - + <waitForElementVisible selector="{{AdminEditCustomerAddressesSection.dropDownAttribute(customerAddressAttribute.code)}}" stepKey="waitForSelectOption"/> <selectOption selector="{{AdminEditCustomerAddressesSection.dropDownAttribute(customerAddressAttribute.code)}}" userInput="{{optionValue}}" stepKey="selectOptionValue"/> <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> <waitForPageLoad stepKey="waitForAddressSaved"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml new file mode 100644 index 0000000000000..469e27129d2c5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChangeCustomerAssociatedWebsiteTest"> + <annotations> + <features value="Customer"/> + <title value="Admin should be able to change customer associated website ID"/> + <description value="Admin should be able to change customer associated website ID"/> + <severity value="AVERAGE"/> + <useCaseId value="MC-38913"/> + <testCaseId value="MC-39764"/> + <stories value="Customer Edit"/> + <group value="customer"/> + </annotations> + <before> + <!--Login to admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create second website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> + <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> + </actionGroup> + <!--Create store group and associate it to second website--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <!--Create store view and associate it to second store group--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!--Delete customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Delete custom website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <!--Logout from admin--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!--Open customer edit page--> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <!--Navigate to "Account Information" tab--> + <actionGroup ref="AdminOpenAccountInformationTabFromCustomerEditPageActionGroup" stepKey="openAccountInformationEditPage"/> + <!--Verify that "Main Website" is selected in website selector--> + <seeOptionIsSelected selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{_defaultWebsite.name}}" stepKey="assertThatMainWebsiteIsSelected"/> + <!--Change customer website to "Second Website"--> + <actionGroup ref="AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup" stepKey="updateCustomerWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <!--Verify that changes are saved successfully--> + <actionGroup ref="AdminSaveCustomerAndAssertSuccessMessage" stepKey="assertThatChangesAreSavedSuccessfully"/> + <!--Open customer edit page--> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage2"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <!--Navigate to "Account Information" tab--> + <actionGroup ref="AdminOpenAccountInformationTabFromCustomerEditPageActionGroup" stepKey="openAccountInformationEditPage2"/> + <!--Verify that "Second Website" is selected in website selector--> + <seeOptionIsSelected selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{NewWebSiteData.name}}" stepKey="assertThatSecondWebsiteIsSelected"/> + </test> +</tests> diff --git a/app/code/Magento/CustomerGraphQl/Model/Context/AddCustomerGroupToContext.php b/app/code/Magento/CustomerGraphQl/Model/Context/AddCustomerGroupToContext.php new file mode 100644 index 0000000000000..aaa2b85636f79 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Context/AddCustomerGroupToContext.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Context; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\GraphQl\Model\Query\ContextParametersInterface; +use Magento\GraphQl\Model\Query\ContextParametersProcessorInterface; +use Magento\Customer\Model\Group; +use Magento\Customer\Model\Group\Resolver as CustomerGroupResolver; + +/** + * @inheritdoc + */ +class AddCustomerGroupToContext implements ContextParametersProcessorInterface +{ + /** + * @var CustomerGroupResolver + */ + private $customerGroupResolver; + + /** + * @param CustomerGroupResolver $customerGroupResolver + */ + public function __construct( + CustomerGroupResolver $customerGroupResolver + ) { + $this->customerGroupResolver = $customerGroupResolver; + } + + /** + * @inheritdoc + */ + public function execute(ContextParametersInterface $contextParameters): ContextParametersInterface + { + $customerGroupId = null; + $extensionAttributes = $contextParameters->getExtensionAttributesData(); + if ($contextParameters->getUserType() === UserContextInterface::USER_TYPE_GUEST) { + $customerGroupId = Group::NOT_LOGGED_IN_ID; + } elseif (!empty($extensionAttributes) && $extensionAttributes['is_customer'] === true) { + $customerGroupId = $this->customerGroupResolver->resolve((int) $contextParameters->getUserId()); + } + if ($customerGroupId !== null) { + $contextParameters->addExtensionAttribute('customer_group_id', (int) $customerGroupId); + } + return $contextParameters; + } +} diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index 3ed77a2ad563c..3e3a5327370a5 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -17,6 +17,7 @@ <arguments> <argument name="contextParametersProcessors" xsi:type="array"> <item name="add_user_info_to_context" xsi:type="object">Magento\CustomerGraphQl\Model\Context\AddUserInfoToContext</item> + <item name="add_customer_group_to_context" xsi:type="object">Magento\CustomerGraphQl\Model\Context\AddCustomerGroupToContext</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 9781b521597ba..513e15c2a4dc4 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -1756,7 +1756,7 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') foreach ($package['items'] as $item) { $content[] = $item['name']; } - $nodePiece->addChild('PieceContents', substr(implode(',', $content), 0, 34)); + $nodePiece->addChild('PieceContents', $this->string->substr(implode(',', $content), 0, 34)); } $nodeShipmentDetails->addChild('Weight', sprintf('%.3f', $rawRequest->getPackageWeight())); @@ -1776,7 +1776,7 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') $nodeShipmentDetails->addChild('DoorTo', 'DD'); $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); $contentType = isset($package['params']['container']) ? $package['params']['container'] : ''; - $packageType = $contentType === self::DHL_CONTENT_TYPE_NON_DOC ? 'CP' : ''; + $packageType = $contentType === self::DHL_CONTENT_TYPE_NON_DOC ? 'CP' : 'EE'; $nodeShipmentDetails->addChild('PackageType', $packageType); if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { $nodeShipmentDetails->addChild('IsDutiable', 'Y'); diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml index b89aa7d126686..2ced91731e4ba 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml @@ -30,9 +30,8 @@ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="createDownloadableProduct"/> </createData> - - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'indexer:reindex' command for preserving Backward Compatibility" stepKey="reindex"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signIn"> @@ -51,8 +50,8 @@ <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'indexer:reindex' command for preserving Backward Compatibility" stepKey="reindex"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> </after> <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" stepKey="addSimpleProductToCart"> diff --git a/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php b/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php new file mode 100644 index 0000000000000..fdc71faa90902 --- /dev/null +++ b/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Eav\Model\Mview; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Sql\Expression; +use Magento\Framework\Mview\View\ChangeLogBatchWalkerInterface; +use Magento\Framework\Mview\View\ChangelogInterface; + +/** + * Class BatchIterator + */ +class ChangeLogBatchWalker implements ChangeLogBatchWalkerInterface +{ + private const GROUP_CONCAT_MAX_VARIABLE = 'group_concat_max_len'; + /** ID is defined as small int. Default size of it is 5 */ + private const DEFAULT_ID_SIZE = 5; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var array + */ + private $entityTypeCodes; + + /** + * @param ResourceConnection $resourceConnection + * @param array $entityTypeCodes + */ + public function __construct( + ResourceConnection $resourceConnection, + array $entityTypeCodes = [] + ) { + $this->resourceConnection = $resourceConnection; + $this->entityTypeCodes = $entityTypeCodes; + } + + /** + * Calculate EAV attributes size + * + * @param ChangelogInterface $changelog + * @return int + * @throws \Exception + */ + private function calculateEavAttributeSize(ChangelogInterface $changelog): int + { + $connection = $this->resourceConnection->getConnection(); + + if (!isset($this->entityTypeCodes[$changelog->getViewId()])) { + throw new \Exception('Entity type for view was not defined'); + } + + $select = $connection->select(); + $select->from( + $this->resourceConnection->getTableName('eav_attribute'), + new Expression('COUNT(*)') + ) + ->joinInner( + ['type' => $connection->getTableName('eav_entity_type')], + 'type.entity_type_id=eav_attribute.entity_type_id' + ) + ->where('type.entity_type_code = ?', $this->entityTypeCodes[$changelog->getViewId()]); + + return (int) $connection->fetchOne($select); + } + + /** + * Prepare group max concat + * + * @param int $numberOfAttributes + * @return void + * @throws \Exception + */ + private function setGroupConcatMax(int $numberOfAttributes): void + { + $connection = $this->resourceConnection->getConnection(); + $connection->query(sprintf( + 'SET SESSION %s=%s', + self::GROUP_CONCAT_MAX_VARIABLE, + $numberOfAttributes * (self::DEFAULT_ID_SIZE + 1) + )); + } + + /** + * @inheritdoc + * @throws \Exception + */ + public function walk(ChangelogInterface $changelog, int $fromVersionId, int $toVersion, int $batchSize) + { + $connection = $this->resourceConnection->getConnection(); + $numberOfAttributes = $this->calculateEavAttributeSize($changelog); + $this->setGroupConcatMax($numberOfAttributes); + $select = $connection->select()->distinct(true) + ->where( + 'version_id > ?', + (int) $fromVersionId + ) + ->where( + 'version_id <= ?', + $toVersion + ) + ->group([$changelog->getColumnName(), 'store_id']) + ->limit($batchSize); + + $columns = [ + $changelog->getColumnName(), + 'attribute_ids' => new Expression('GROUP_CONCAT(attribute_id)'), + 'store_id' + ]; + $select->from($changelog->getName(), $columns); + return $connection->fetchAll($select); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/events.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/events.xml similarity index 60% rename from app/code/Magento/CatalogUrlRewrite/etc/adminhtml/events.xml rename to app/code/Magento/GiftMessageGraphQl/etc/graphql/events.xml index 9c4a8aaf41231..2411221ded375 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/events.xml +++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/events.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> - <event name="catalog_product_to_website_change"> - <observer name="catalog_product_to_website_change" instance="Magento\CatalogUrlRewrite\Observer\ProductToWebsiteChangeObserver"/> + <event name="sales_model_service_quote_submit_before"> + <observer name="giftmessage" instance="Magento\GiftMessage\Observer\SalesEventQuoteSubmitBeforeObserver" shared="false" /> </event> </config> diff --git a/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml b/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml index d700b0e9e7668..6d8d42a5d3f8f 100644 --- a/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml +++ b/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml @@ -14,6 +14,11 @@ <value id="google_analytics" type="host">www.google-analytics.com</value> </values> </policy> + <policy id="connect-src"> + <values> + <value id="google_analytics" type="host">www.google-analytics.com</value> + </values> + </policy> <policy id="img-src"> <values> <value id="google_ad_services" type="host">www.googleadservices.com</value> diff --git a/app/code/Magento/GroupedProduct/Api/Data/GroupedOptionsInterface.php b/app/code/Magento/GroupedProduct/Api/Data/GroupedOptionsInterface.php new file mode 100644 index 0000000000000..9278ef168eb0d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Api/Data/GroupedOptionsInterface.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Represents `product item id with qty` of a grouped product. + */ +interface GroupedOptionsInterface extends ExtensibleDataInterface +{ + /** + * Get associated product id + * + * @return int|null + */ + public function getId(): ?int; + + /** + * Get associated product qty + * + * @return int|null + */ + public function getQty(): ?int; + + /** + * Set extension attributes + * + * @param \Magento\GroupedProduct\Api\Data\GroupedOptionsExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes(GroupedOptionsExtensionInterface $extensionAttributes): void; + + /** + * Get extension attributes + * + * @return \Magento\GroupedProduct\Api\Data\GroupedOptionsExtensionInterface|null + */ + public function getExtensionAttributes(): ?GroupedOptionsExtensionInterface; +} diff --git a/app/code/Magento/GroupedProduct/Model/Quote/Item/CartItemProcessor.php b/app/code/Magento/GroupedProduct/Model/Quote/Item/CartItemProcessor.php new file mode 100644 index 0000000000000..78ab91194992e --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Quote/Item/CartItemProcessor.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Quote\Item; + +use Magento\Framework\DataObject; +use Magento\Framework\DataObject\Factory as ObjectFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\GroupedProduct\Api\Data\GroupedOptionsInterface; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\Quote\Api\Data as QuoteApi; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Model\Quote\Item\CartItemProcessorInterface; + +/** + * Converts grouped_options to super_group for the grouped product. + */ +class CartItemProcessor implements CartItemProcessorInterface +{ + private const SUPER_GROUP_CODE = 'super_group'; + + /** + * @var ObjectFactory + */ + private $objectFactory; + + /** + * @var QuoteApi\ProductOptionExtensionFactory + */ + private $productOptionExtensionFactory; + + /** + * @var QuoteApi\ProductOptionInterfaceFactory + */ + private $productOptionFactory; + + /** + * @var array|null + */ + private $groupedOptions; + + /** + * @param ObjectFactory $objectFactory + * @param QuoteApi\ProductOptionExtensionFactory $productOptionExtensionFactory + * @param QuoteApi\ProductOptionInterfaceFactory $productOptionFactory + */ + public function __construct( + ObjectFactory $objectFactory, + QuoteApi\ProductOptionExtensionFactory $productOptionExtensionFactory, + QuoteApi\ProductOptionInterfaceFactory $productOptionFactory + ) { + $this->objectFactory = $objectFactory; + $this->productOptionExtensionFactory = $productOptionExtensionFactory; + $this->productOptionFactory = $productOptionFactory; + } + + /** + * Converts the grouped_options request data into the same format as native frontend add-to-cart + * + * @param CartItemInterface $cartItem + * @return DataObject|null + */ + public function convertToBuyRequest(CartItemInterface $cartItem): ?DataObject + { + if ($cartItem->getProductOption() + && $cartItem->getProductOption()->getExtensionAttributes() + && $cartItem->getProductOption()->getExtensionAttributes()->getGroupedOptions() + ) { + $groupedOptions = $cartItem->getProductOption()->getExtensionAttributes()->getGroupedOptions(); + $this->groupedOptions = $groupedOptions; + + return $this->objectFactory->create($this->getConvertedData($groupedOptions)); + } + + return null; + } + + /** + * Returns grouped_options converted to super_group data + * + * @param GroupedOptionsInterface[] $groupedOptions + * @return array + * @throws LocalizedException + */ + private function getConvertedData(array $groupedOptions): array + { + $requestData = []; + foreach ($groupedOptions as $item) { + /** @var GroupedOptionsInterface $item */ + if ($item->getQty() === null || $item->getId() === null) { + throw new LocalizedException(__('Please specify id and qty for grouped options.')); + } + $requestData[self::SUPER_GROUP_CODE][$item->getId()] = $item->getQty(); + } + + return $requestData; + } + + /** + * Option processor + * + * @param CartItemInterface $cartItem + * @return CartItemInterface + */ + public function processOptions(CartItemInterface $cartItem): CartItemInterface + { + if (empty($this->groupedOptions) || $cartItem->getProductType() !== Grouped::TYPE_CODE) { + return $cartItem; + } + + $extension = $this->productOptionExtensionFactory->create() + ->setGroupedOptions($this->groupedOptions); + if (!$cartItem->getProductOption()) { + $cartItem->setProductOption($this->productOptionFactory->create()); + } + $cartItem->getProductOption()->setExtensionAttributes($extension); + + return $cartItem; + } +} diff --git a/app/code/Magento/GroupedProduct/Model/Quote/Item/GroupedOptions.php b/app/code/Magento/GroupedProduct/Model/Quote/Item/GroupedOptions.php new file mode 100644 index 0000000000000..3b249a7b5d548 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Quote/Item/GroupedOptions.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Quote\Item; + +use Magento\GroupedProduct\Api\Data\GroupedOptionsInterface; +use Magento\GroupedProduct\Api\Data\GroupedOptionsExtensionInterface; + +/** + * @inheritDoc + */ +class GroupedOptions implements GroupedOptionsInterface +{ + /** + * @var int|null + */ + private $qty; + + /** + * @var int|null + */ + private $id; + + /** + * @var GroupedOptionsExtensionInterface|null + */ + private $extensionAttributes; + + /** + * @param int|null $id + * @param int|null $qty + * @param GroupedOptionsExtensionInterface|null $extensionAttributes + */ + public function __construct( + ?int $id = null, + ?int $qty = null, + ?GroupedOptionsExtensionInterface $extensionAttributes = null + ) { + $this->id = $id; + $this->qty = $qty; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritDoc + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * @inheritDoc + */ + public function getQty(): ?int + { + return $this->qty; + } + + /** + * @inheritDoc + */ + public function setExtensionAttributes(GroupedOptionsExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritDoc + */ + public function getExtensionAttributes(): ?GroupedOptionsExtensionInterface + { + return $this->extensionAttributes; + } +} diff --git a/app/code/Magento/GroupedProduct/Model/ResourceModel/Indexer/Stock/Grouped.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Indexer/Stock/Grouped.php index 9d29b02f68bf1..b2a8a361564e0 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Indexer/Stock/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Indexer/Stock/Grouped.php @@ -94,7 +94,7 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f $select->columns(['status' => $stockStatusExpr]); if ($entityIds !== null) { - $select->where('e.entity_id IN(?)', $entityIds); + $select->where('e.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } return $select; diff --git a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php index c5f0316feb126..4c24cdb752d3c 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php @@ -166,7 +166,7 @@ private function prepareGroupedProductPriceDataSelect(array $dimensions, array $ ); if ($entityIds !== null) { - $select->where('e.entity_id IN(?)', $entityIds); + $select->where('e.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } return $select; diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminAssertProductOnGroupedOptionGridActionGroup.xml b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminAssertProductOnGroupedOptionGridActionGroup.xml new file mode 100644 index 0000000000000..2f706f45f834d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminAssertProductOnGroupedOptionGridActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertProductOnGroupedOptionGridActionGroup"> + <annotations> + <description>Admin assert product on grouped option grid.</description> + </annotations> + <arguments> + <argument name="product"/> + </arguments> + + <grabTextFrom selector="{{AdminGroupedProductOptionGridSection.productName}}" stepKey="grabProductName"/> + <assertEquals stepKey="assertProductName"> + <expectedResult type="string">{{product.name}}</expectedResult> + <actualResult type="variable">$grabProductName</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminGroupedProductOptionGridSection.productSku}}" stepKey="grabProductSku"/> + <assertEquals stepKey="assertProductSku"> + <expectedResult type="string">{{product.sku}}</expectedResult> + <actualResult type="variable">$grabProductSku</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminGroupedProductOptionGridSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminGroupedProductOptionGridSection.xml new file mode 100644 index 0000000000000..1fff52c1d30cd --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminGroupedProductOptionGridSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminGroupedProductOptionGridSection"> + <element name="productName" type="text" selector=".data-row td[data-index='name']"/> + <element name="productSku" type="text" selector=".data-row td[data-index='sku']"/> + </section> +</sections> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml new file mode 100644 index 0000000000000..d5dcd7f48b956 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateGroupedProductNonDefaultAttributeSetTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Create product"/> + <title value="Create Grouped Product when non-default attribute set is chosen"/> + <description value="Create Grouped Product with simple when non-default attribute set is chosen"/> + <testCaseId value="MC-39950"/> + <severity value="MAJOR"/> + <group value="groupedProduct"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="createSimpleProduct"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + </after> + + <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createGroupedProduct"> + <argument name="productType" value="grouped"/> + </actionGroup> + <actionGroup ref="AdminProductPageSelectAttributeSetActionGroup" stepKey="selectAttributeSet"> + <argument name="attributeSetName" value="$createAttributeSet.attribute_set_name$"/> + </actionGroup> + <actionGroup ref="FillGroupedProductFormActionGroup" stepKey="fillProductForm"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + <actionGroup ref="AdminAssignProductToGroupActionGroup" stepKey="addSimpleToGroup"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AdminAssertProductOnGroupedOptionGridActionGroup" stepKey="assertProductOptionGrid"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php index 3ea8c6eb3c2b9..12a21a4ebd04b 100644 --- a/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php +++ b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php @@ -562,6 +562,7 @@ protected function fillMeta() 'fit' => true, 'label' => __('Thumbnail'), 'sortOrder' => 20, + 'labelVisible' => false, ], ], ], @@ -586,6 +587,7 @@ protected function fillMeta() 'validation' => [ 'validate-number' => true, ], + 'labelVisible' => false, ], ], ], @@ -601,7 +603,8 @@ protected function fillMeta() 'elementTmpl' => 'Magento_GroupedProduct/components/position', 'sortOrder' => 90, 'fit' => true, - 'dataScope' => 'positionCalculated' + 'dataScope' => 'positionCalculated', + 'labelVisible' => false, ], ], ], @@ -660,6 +663,7 @@ protected function getTextColumn($dataScope, $fit, Phrase $label, $sortOrder) 'fit' => $fit, 'label' => $label, 'sortOrder' => $sortOrder, + 'labelVisible' => false, ], ], ], diff --git a/app/code/Magento/GroupedProduct/etc/di.xml b/app/code/Magento/GroupedProduct/etc/di.xml index 924d2d1fc9669..8b58ac08ebd31 100644 --- a/app/code/Magento/GroupedProduct/etc/di.xml +++ b/app/code/Magento/GroupedProduct/etc/di.xml @@ -6,6 +6,8 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\GroupedProduct\Api\Data\GroupedOptionsInterface" type="Magento\GroupedProduct\Model\Quote\Item\GroupedOptions" /> + <type name="Magento\Quote\Model\Quote\Item\RelatedProducts"> <arguments> <argument name="relatedProductTypes" xsi:type="array"> @@ -105,6 +107,13 @@ </argument> </arguments> </type> + <type name="Magento\Quote\Model\Quote\Item\Repository"> + <arguments> + <argument name="cartItemProcessors" xsi:type="array"> + <item name="grouped" xsi:type="object">Magento\GroupedProduct\Model\Quote\Item\CartItemProcessor\Proxy</item> + </argument> + </arguments> + </type> <type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver"> <arguments> <argument name="parentItemProcessorPool" xsi:type="array"> diff --git a/app/code/Magento/GroupedProduct/etc/extension_attributes.xml b/app/code/Magento/GroupedProduct/etc/extension_attributes.xml index 14ff9821025c4..b614a1277de76 100644 --- a/app/code/Magento/GroupedProduct/etc/extension_attributes.xml +++ b/app/code/Magento/GroupedProduct/etc/extension_attributes.xml @@ -9,4 +9,8 @@ <extension_attributes for="Magento\Catalog\Api\Data\ProductLinkInterface"> <attribute code="qty" type="float" /> </extension_attributes> + + <extension_attributes for="Magento\Quote\Api\Data\ProductOptionInterface"> + <attribute code="grouped_options" type="Magento\GroupedProduct\Api\Data\GroupedOptionsInterface[]" /> + </extension_attributes> </config> diff --git a/app/code/Magento/Indexer/Model/ProcessManager.php b/app/code/Magento/Indexer/Model/ProcessManager.php index 2f2c500e028cf..2b25c8c6a3d15 100644 --- a/app/code/Magento/Indexer/Model/ProcessManager.php +++ b/app/code/Magento/Indexer/Model/ProcessManager.php @@ -7,6 +7,9 @@ namespace Magento\Indexer\Model; +use Magento\Framework\App\ObjectManager; +use Psr\Log\LoggerInterface; + /** * Provide functionality for executing user functions in multi-thread mode. */ @@ -29,15 +32,22 @@ class ProcessManager /** @var int|null */ private $threadsCount; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Framework\Registry $registry * @param int|null $threadsCount + * @param LoggerInterface|null $logger */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Framework\Registry $registry = null, - int $threadsCount = null + int $threadsCount = null, + LoggerInterface $logger = null ) { $this->resource = $resource; if (null === $registry) { @@ -47,6 +57,9 @@ public function __construct( } $this->registry = $registry; $this->threadsCount = (int)$threadsCount; + $this->logger = $logger ?? ObjectManager::getInstance()->get( + LoggerInterface::class + ); } /** @@ -135,11 +148,20 @@ private function isSetupMode(): bool */ private function startChildProcess(callable $userFunction) { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $status = call_user_func($userFunction); - $status = is_int($status) ? $status : 0; - // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage - exit($status); + try { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $status = call_user_func($userFunction); + $status = is_int($status) ? $status : 0; + } catch (\Throwable $e) { + $status = 1; + $this->logger->error( + __('Child process failed with message: %1', $e->getMessage()), + ['exception' => $e] + ); + } finally { + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage + exit($status); + } } /** diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml index d474094dcd54b..e7e7ba82bf09c 100644 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml @@ -9,6 +9,15 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminReindexAndFlushCache"> <annotations> + <!-- + PLEASE NOTE: + The action group runs commands to reindex ALL indexers and to flush ALL cache types. + It's better to specify needed index (for reindexing) / cache type (for cache flushing). + Please use the following action groups: + - CliIndexerReindexActionGroup - run reindex by CLI with specified indexers + - CliCacheCleanActionGroup - run cache:clean by CLI with specified cache tags + - CliCacheFlushActionGroup - run cache:flush by CLI with specified cache tags + --> <description>Run reindex and flush cache.</description> </annotations> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml index c083383dd8861..f5919c6ccbb80 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> @@ -38,7 +38,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Admin Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml index 1175103395427..09e48b5c61aaf 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml @@ -28,7 +28,7 @@ <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="enableAddStoreCodeToUrls"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> </before> @@ -43,7 +43,7 @@ <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="disableAddStoreCodeToUrls"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml index f9418a9cf1e1b..3b2f61339b921 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml @@ -26,7 +26,7 @@ <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="enableAddStoreCodeToUrls"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createCustomWebsite"> <argument name="newWebsiteName" value="{{customWebsite.name}}"/> @@ -60,7 +60,7 @@ <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="disableAddStoreCodeToUrls"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml index cf90f0b6a8511..3a80bbb7a6f2e 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAdmin"/> </before> @@ -32,7 +32,7 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer Login from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml index 4ef72d949065d..e4a6767ce7b22 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml @@ -19,7 +19,7 @@ </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml index 5b5e9e21113c8..6ae6ddfeccb47 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml @@ -24,7 +24,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="NewAdminUser" stepKey="createNewAdmin"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createFirstCustomer"/> <createData entity="Simple_US_CA_Customer_Assistance_Allowed" stepKey="createSecondCustomer"/> @@ -40,7 +40,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login into First Customer account --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml index e4f0209c55233..8493fda17636a 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml @@ -29,7 +29,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 1" stepKey="enableLoginAsCustomerManualChoose"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createCustomStore"> <argument name="website" value="{{_defaultWebsite.name}}"/> @@ -54,7 +54,7 @@ stepKey="disableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml index 5f706a814eb71..551139fb8095e 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml @@ -25,7 +25,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 1" stepKey="enableLoginAsCustomerManualChoose"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createCustomStore"> <argument name="website" value="{{_defaultWebsite.name}}"/> @@ -50,7 +50,7 @@ stepKey="disableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml index 79c7571a08cfb..ac2790940b86f 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml @@ -23,14 +23,14 @@ </annotations> <before> - <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> - <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableFreeShippingMethodActionGroup" stepKey="enableFreeShipping"/> + <actionGroup ref="CliEnableFlatRateShippingMethodActionGroup" stepKey="enableFlatRateShipping"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="SimpleProduct2" stepKey="createProduct1"/> <createData entity="SimpleProduct2" stepKey="createProduct2"/> <createData entity="Simple_US_Customer_Assistance_Allowed_Two_Addresses" stepKey="createCustomer"/> @@ -43,8 +43,8 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> - <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml index 8169b9df4c43d..8afaaabbc92bf 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> @@ -59,7 +59,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml index 11d622319af33..8bef9fce9995b 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> @@ -59,7 +59,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login to storefront as Customer --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml index bc4c4adc3ac5a..1c3ae6e790089 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> @@ -31,7 +31,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Admin Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml index e7b5de55a56cb..ae99a4dda5593 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml @@ -24,7 +24,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> </before> @@ -33,7 +33,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login into Customer account --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml index 5bbc218e0a948..51a4d1cf9fbed 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml @@ -22,7 +22,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createFirstCustomer"/> <createData entity="Simple_US_CA_Customer_Assistance_Allowed" stepKey="createSecondCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> @@ -33,7 +33,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login into First Customer account --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml index 50513797d06e9..9c95ab3c3c5cc 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml @@ -24,7 +24,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> @@ -67,7 +67,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as new User --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml index d48f167656301..4032aff6d122f 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml @@ -26,7 +26,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> @@ -70,7 +70,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as new User --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml index e1ea363bdf6bc..10bff7c2ef68f 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml @@ -19,7 +19,7 @@ </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml index be2749e64f65c..38e89050c275e 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml @@ -33,7 +33,8 @@ <after> <closeTab stepKey="closeLoginAsCustomerTab"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml index 351a3c569ce24..6a83e820039d8 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml @@ -24,7 +24,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> <after> @@ -32,7 +32,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml index 7765975863485..5551ea1baba70 100644 --- a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml @@ -11,20 +11,18 @@ use Magento\LoginAsCustomerAssistance\ViewModel\ShoppingAssistanceViewModel; /** @var Escaper $escaper */ /** @var ShoppingAssistanceViewModel $viewModel */ $viewModel = $block->getViewModel(); -?> -<script type="text/x-magento-init"> -{ - ".form-create-account, .form-edit-account": { - "Magento_LoginAsCustomerAssistance/js/opt-in": { - "allowAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::ALLOWED ?>", - "denyAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::DENIED ?>" +if ($viewModel->isLoginAsCustomerEnabled()): ?> + <script type="text/x-magento-init"> + { + ".form-create-account, .form-edit-account": { + "Magento_LoginAsCustomerAssistance/js/opt-in": { + "allowAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::ALLOWED ?>", + "denyAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::DENIED ?>" + } } } -} -</script> - -<?php if ($viewModel->isLoginAsCustomerEnabled()): ?> + </script> <div class="field choice"> <input type="checkbox" name="assistance_allowed_checkbox" diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php index f61e34512bfe3..57a59ad800469 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -33,8 +33,8 @@ class Asset extends Select * @param UiComponentFactory $uiComponentFactory * @param FilterBuilder $filterBuilder * @param FilterModifier $filterModifier - * @param OptionSourceInterface $optionsProvider * @param GetContentByAssetIdsInterface $getContentIdentities + * @param OptionSourceInterface $optionsProvider * @param array $components * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -44,8 +44,8 @@ public function __construct( UiComponentFactory $uiComponentFactory, FilterBuilder $filterBuilder, FilterModifier $filterModifier, - OptionSourceInterface $optionsProvider = null, GetContentByAssetIdsInterface $getContentIdentities, + OptionSourceInterface $optionsProvider = null, array $components = [], array $data = [] ) { diff --git a/app/code/Magento/MediaStorage/Service/ImageResize.php b/app/code/Magento/MediaStorage/Service/ImageResize.php index d5ce1a7e20f98..9b0adcd161339 100644 --- a/app/code/Magento/MediaStorage/Service/ImageResize.php +++ b/app/code/Magento/MediaStorage/Service/ImageResize.php @@ -195,14 +195,18 @@ public function resizeFromThemes(array $themes = null): Generator $this->fileStorageDatabase->saveFileToFilesystem($mediastoragefilename); } if ($this->mediaDirectory->isFile($originalImagePath)) { - foreach ($viewImages as $viewImage) { - $this->resize($viewImage, $originalImagePath, $originalImageName); + try { + foreach ($viewImages as $viewImage) { + $this->resize($viewImage, $originalImagePath, $originalImageName); + } + } catch (\Exception $e) { + $error = $e->getMessage(); } } else { $error = __('Cannot resize image "%1" - original image not found', $originalImagePath); } - yield ['filename' => $originalImageName, 'error' => $error] => $count; + yield ['filename' => $originalImageName, 'error' => (string) $error] => $count; } } @@ -276,6 +280,7 @@ private function getUniqueImageIndex(array $imageData): string * @param string $originalImagePath * @param array $imageParams * @return Image + * @throws \InvalidArgumentException */ private function makeImage(string $originalImagePath, array $imageParams): Image { diff --git a/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php b/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php index b8c4aded8a047..5df2269f8f80e 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php @@ -60,11 +60,6 @@ class ImageResizeTest extends TestCase */ protected $imageFactoryMock; - /** - * @var Image|MockObject - */ - protected $imageMock; - /** * @var ParamsBuilder|MockObject */ @@ -141,7 +136,6 @@ protected function setUp(): void $this->appStateMock = $this->createMock(State::class); $this->imageConfigMock = $this->createMock(MediaConfig::class); $this->productImageMock = $this->createMock(ProductImage::class); - $this->imageMock = $this->createMock(Image::class); $this->imageFactoryMock = $this->createMock(ImageFactory::class); $this->paramsBuilderMock = $this->createMock(ParamsBuilder::class); $this->viewMock = $this->createMock(View::class); @@ -164,9 +158,6 @@ protected function setUp(): void ->with(DirectoryList::MEDIA) ->willReturn($this->mediaDirectoryMock); - $this->imageFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->imageMock); $this->assetImageMock->expects($this->any()) ->method('getPath') ->willReturn($this->testfilepath); @@ -256,6 +247,11 @@ public function testResizeFromThemesMediaStorageDatabase() ->method('fileExists') ->willReturn(false); + $imageMock = $this->createMock(Image::class); + $this->imageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($imageMock); + $this->productImageMock->expects($this->any()) ->method('getCountUsedProductImages') ->willReturn(1); @@ -284,6 +280,49 @@ function () { $generator = $this->service->resizeFromThemes(['test-theme']); while ($generator->valid()) { + $resizeInfo = $generator->key(); + $this->assertEquals('image.jpg', $resizeInfo['filename']); + $this->assertEmpty($resizeInfo['error']); + $generator->next(); + } + } + + public function testResizeFromThemesUnsupportedImage() + { + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->willReturn(true); + $this->databaseMock->expects($this->any()) + ->method('fileExists') + ->willReturn(false); + + $this->imageFactoryMock->expects($this->once()) + ->method('create') + ->willThrowException(new \InvalidArgumentException('Unsupported image format.')); + + $this->productImageMock->expects($this->any()) + ->method('getCountUsedProductImages') + ->willReturn(1); + $this->productImageMock->expects($this->any()) + ->method('getUsedProductImages') + ->willReturnCallback( + function () { + $data = [[ 'filepath' => $this->testfilename ]]; + foreach ($data as $e) { + yield $e; + } + } + ); + + $this->mediaDirectoryMock->expects($this->any()) + ->method('isFile') + ->with($this->testfilepath) + ->willReturn(true); + + $generator = $this->service->resizeFromThemes(['test-theme']); + while ($generator->valid()) { + $resizeInfo = $generator->key(); + $this->assertEquals('Unsupported image format.', $resizeInfo['error']); $generator->next(); } } @@ -297,6 +336,11 @@ public function testResizeFromImageNameMediaStorageDatabase() ->method('fileExists') ->willReturn(false); + $imageMock = $this->createMock(Image::class); + $this->imageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($imageMock); + $this->mediaDirectoryMock->expects($this->any()) ->method('isFile') ->with($this->testfilepath) diff --git a/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php b/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php index ea3e765ebb5cc..03587f71036f2 100644 --- a/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php +++ b/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php @@ -74,6 +74,7 @@ public function beforeDispatch(Cart $subject, RequestInterface $request) /** @var Quote $quote */ $quote = $this->checkoutSession->getQuote(); if ($quote->isMultipleShippingAddresses() && $this->isCheckoutComplete()) { + $this->disableMultishipping->execute($quote); foreach ($quote->getAllShippingAddresses() as $address) { $quote->removeAddress($address->getId()); } diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php index 8845395be406e..8bfff09aa73b2 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php @@ -1185,7 +1185,9 @@ private function validateMinimumAmountForAddressItems() $baseTotal = 0; foreach ($addresses as $address) { - $taxes = $taxInclude ? $address->getBaseTaxAmount() : 0; + $taxes = $taxInclude + ? $address->getBaseTaxAmount() + $address->getBaseDiscountTaxCompensationAmount() + : 0; $baseTotal += $address->getBaseSubtotalWithDiscount() + $taxes; } diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontNavigateToShippingInformationPageActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontNavigateToShippingInformationPageActionGroup.xml new file mode 100644 index 0000000000000..1ca3b20af6a84 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontNavigateToShippingInformationPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontNavigateToShippingInformationPageActionGroup"> + <annotations> + <description>Navigate to shipping information page. Starts on multishipping addressees page.</description> + </annotations> + + <waitForElementVisible selector="{{SingleShippingSection.goToShippingInfo}}" stepKey="waitForButton"/> + <click selector="{{SingleShippingSection.goToShippingInfo}}" stepKey="goToShippingInformation"/> + <waitForPageLoad stepKey="waitForShippingInfoPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontSelectMultipleAddressesOnCheckoutActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontSelectMultipleAddressesOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..f5e76a0d146fc --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontSelectMultipleAddressesOnCheckoutActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectMultipleAddressesOnCheckoutActionGroup"> + <annotations> + <description>Select addresses for multinshipping checkout. Start on multishipping addresses page.</description> + </annotations> + <arguments> + <argument name="addressOption1" type="string" defaultValue="1"/> + <argument name="addressOption2" type="string" defaultValue="2"/> + </arguments> + + <waitForElementVisible selector="{{MultishippingSection.shippingAddressOptions(addressOption1,addressOption1)}}" stepKey="waitForMultishippingPage"/> + <grabTextFrom selector="{{MultishippingSection.shippingAddressOptions(addressOption1,addressOption1)}}" stepKey="firstShippingAddressValue"/> + <selectOption selector="{{MultishippingSection.shippingAddressSelector(addressOption1)}}" userInput="{$firstShippingAddressValue}" stepKey="selectFirstShippingMethod"/> + <waitForPageLoad after="selectFirstShippingMethod" stepKey="waitForSecondShippingAddresses"/> + <grabTextFrom selector="{{MultishippingSection.shippingAddressOptions(addressOption2,addressOption2)}}" stepKey="secondShippingAddressValue"/> + <selectOption selector="{{MultishippingSection.shippingAddressSelector(addressOption2)}}" userInput="{$secondShippingAddressValue}" stepKey="selectSecondShippingMethod"/> + <click selector="{{SingleShippingSection.updateAddress}}" stepKey="clickOnUpdateAddress"/> + <waitForPageLoad stepKey="waitForShippingInformation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml index 7ae23e8f871eb..5e1c14c57f533 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml @@ -30,7 +30,7 @@ <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml index 27876df8caefe..dcdc9203a2075 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml @@ -30,7 +30,7 @@ <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml index f21a8d32d8841..043d0fc41abe7 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml @@ -30,7 +30,7 @@ <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml new file mode 100644 index 0000000000000..f0a97d240aa69 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCreateOrderWithMultishippingAfterReturningToCartTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Checkout with multiple addresses."/> + <title value="Checkout with multiple addresses after returning on cart page during checkout."/> + <description value="Verify customer able to checkout with multiple addresses after returning to cart page and continue checkout with browser 'back' button."/> + <severity value="AVERAGE"/> + <testCaseId value="MC-39583"/> + <useCaseId value="MC-36425"/> + <group value="multishipping"/> + </annotations> + + <before> + <!--Create test data.--> + <createData entity="SimpleProduct2" stepKey="product"/> + <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> + <!--Set up configuration.--> + <actionGroup ref="CliEnableFreeShippingMethodActionGroup" stepKey="enableFreeShipping"/> + <actionGroup ref="CliEnableFlatRateShippingMethodActionGroup" stepKey="enableFlatRateShipping"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> + </before> + + <after> + <!--Clean up test data, revert configuration.--> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> + </after> + + <!--Add product to cart and proceed to multishipping checkout. --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="navigateToProductPage"> + <argument name="productUrl" value="$product.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPageActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$product.name$"/> + <argument name="productQty" value="2"/> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <actionGroup ref="CheckingWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <actionGroup ref="SelectMultiShippingInfoActionGroup" stepKey="checkoutWithMultipleShipping"/> + <waitForPageLoad stepKey="waitForShippingInfoPage"/> + <!--Open cart page before place order.--> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="navigateToCartPage"/> + <waitForPageLoad stepKey="waitForCartPageLoad"/> + <!--Go back to continue checkout with multiple addresses again.--> + <moveBack stepKey="navigateBackToMultishippingCheckout"/> + <actionGroup ref="StorefrontSelectMultipleAddressesOnCheckoutActionGroup" stepKey="selectAddresses"/> + <actionGroup ref="StorefrontNavigateToShippingInformationPageActionGroup" stepKey="navigateToShippingInformationPage"/> + <actionGroup ref="SelectMultiShippingInfoActionGroup" stepKey="checkoutWithMultipleShippingAgain"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPaymentAgain"/> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="checkoutWithPaymentMethodAgain"/> + <actionGroup ref="ReviewOrderForMultiShipmentActionGroup" stepKey="reviewOrderForMultiShipment"> + <argument name="totalNameForFirstOrder" value="Shipping & Handling"/> + <argument name="totalPositionForFirstOrder" value="1"/> + <argument name="totalNameForSecondOrder" value="Shipping & Handling"/> + <argument name="totalPositionForSecondOrder" value="2"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPlaceOrderPageLoad"/> + <actionGroup ref="StorefrontPlaceOrderForMultipleAddressesActionGroup" stepKey="placeOrder"> + <argument name="firstOrderPosition" value="1"/> + <argument name="secondOrderPosition" value="2"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml index 2e5c0acc32053..cdf9c5683c57b 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml @@ -27,9 +27,9 @@ <createData entity="SimpleProduct2" stepKey="createProduct2"/> <createData entity="Simple_US_Customer_Two_Addresses" stepKey="createCustomer"/> <!-- Set configurations --> - <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> - <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableFreeShippingMethodActionGroup" stepKey="enableFreeShipping"/> + <actionGroup ref="CliEnableFlatRateShippingMethodActionGroup" stepKey="enableFlatRateShipping"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$createCustomer$$"/> @@ -42,7 +42,7 @@ <!-- Need logout before customer delete. Fatal error appears otherwise --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml index a696a693fa002..d5ef7e3d5c448 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml @@ -6,18 +6,23 @@ // phpcs:disable Magento2.Files.LineLength ?> -<strong class="product name product-item-name"><a href="<?= $block->escapeUrl($block->getProductUrl()) ?>"><?= $block->escapeHtml($block->getProductName()) ?></a></strong> +<?php +/** @var \Magento\Checkout\Block\Cart\Item\Renderer\ $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<strong class="product name product-item-name"><a href="<?= $escaper->escapeUrl($block->getProductUrl()) ?>"><?= $escaper->escapeHtml($block->getProductName()) ?></a></strong> <?php if ($_options = $block->getOptionList()) : ?> <dl class="item-options"> <?php foreach ($_options as $_option) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <dt><?= $escaper->escapeHtml($_option['label']) ?></dt> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> + <?= $escaper->escapeHtml($_formatedOptionValue['value'], ['span', 'a']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <dl class="item options tooltip content"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= $block->escapeHtml($_formatedOptionValue['full_view'], ['span']) ?></dd> + <dt><?= $escaper->escapeHtml($_option['label']) ?></dt> + <dd><?= $escaper->escapeHtml($_formatedOptionValue['full_view'], ['span']) ?></dd> </dl> <?php endif; ?> </dd> diff --git a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php index 6bdaa40019f8a..d3f8bcb8765c3 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php +++ b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php @@ -244,11 +244,21 @@ public function afterGetById(CustomerRepositoryInterface $subject, CustomerInter */ public function afterGetList(CustomerRepositoryInterface $subject, SearchResults $searchResults): SearchResults { + $customerEmails = []; + + foreach ($searchResults->getItems() as $customer) { + $customerEmails[] = $customer->getEmail(); + } + + $collection = $this->collectionFactory->create(); + $collection->addFieldToFilter('subscriber_email', ['in' => $customerEmails]); + foreach ($searchResults->getItems() as $customer) { /** @var CustomerExtensionInterface $extensionAttributes */ $extensionAttributes = $customer->getExtensionAttributes(); - - $isSubscribed = (int) $extensionAttributes->getIsSubscribed() === Subscriber::STATUS_SUBSCRIBED ?: false; + /** @var Subscriber $subscribe */ + $subscribe = $collection->getItemByColumnValue('subscriber_email', $customer->getEmail()); + $isSubscribed = $subscribe && (int) $subscribe->getStatus() === Subscriber::STATUS_SUBSCRIBED; $extensionAttributes->setIsSubscribed($isSubscribed); } diff --git a/app/code/Magento/Newsletter/etc/extension_attributes.xml b/app/code/Magento/Newsletter/etc/extension_attributes.xml index 09925024e97d5..5c38c02c032b0 100644 --- a/app/code/Magento/Newsletter/etc/extension_attributes.xml +++ b/app/code/Magento/Newsletter/etc/extension_attributes.xml @@ -8,10 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> <extension_attributes for="Magento\Customer\Api\Data\CustomerInterface"> - <attribute code="is_subscribed" type="boolean" > - <join reference_table="newsletter_subscriber" reference_field="customer_id" join_on_field="entity_id"> - <field>subscriber_status</field> - </join> - </attribute> + <attribute code="is_subscribed" type="boolean"/> </extension_attributes> </config> diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliDisableCheckMoneyOrderPaymentMethodActionGroup.xml b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliDisableCheckMoneyOrderPaymentMethodActionGroup.xml new file mode 100644 index 0000000000000..4189a28e6746a --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliDisableCheckMoneyOrderPaymentMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliDisableCheckMoneyOrderPaymentMethodActionGroup"> + <annotations> + <description>Disable Check/Money order payment method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{DisableCheckMoneyOrderPaymentMethod.path}} {{DisableCheckMoneyOrderPaymentMethod.value}}" stepKey="disableCheckMoneyOrderPaymentMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliEnableCheckMoneyOrderPaymentMethodActionGroup.xml b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliEnableCheckMoneyOrderPaymentMethodActionGroup.xml new file mode 100644 index 0000000000000..155633b0772fd --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliEnableCheckMoneyOrderPaymentMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliEnableCheckMoneyOrderPaymentMethodActionGroup"> + <annotations> + <description>Enable Check/Money order payment method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFlatRateShippingMethodActionGroup.xml b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFlatRateShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..3b3e3d597e9f7 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFlatRateShippingMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliDisableFlatRateShippingMethodActionGroup"> + <annotations> + <description>Disable Flat Rate shipping method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{DisableFlatRateShippingMethod.path}} {{DisableFlatRateShippingMethod.value}}" stepKey="disableFlatRateShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFreeShippingMethodActionGroup.xml b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFreeShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..38f2176a95986 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFreeShippingMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliDisableFreeShippingMethodActionGroup"> + <annotations> + <description>Disable Free Shipping method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFlatRateShippingMethodActionGroup.xml b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFlatRateShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..f138a9b616289 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFlatRateShippingMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliEnableFlatRateShippingMethodActionGroup"> + <annotations> + <description>Enable Flat Rate shipping method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFreeShippingMethodActionGroup.xml b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFreeShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..05b7378466b6f --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFreeShippingMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliEnableFreeShippingMethodActionGroup"> + <annotations> + <description>Enable Free Shipping method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml index cea228ac7a344..d43e894b014ff 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -41,7 +41,7 @@ <after> <magentoCLI command="config:set {{StorefrontPaypalEnableTransferCartLineConfigData.path}} {{StorefrontPaypalEnableTransferCartLineConfigData.value}}" stepKey="enableTransferCartLine"/> <magentoCLI command="config:set {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.path}} {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.value}}" stepKey="setPaymentAction"/> - <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> <!-- Delete product --> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml index a4d99ecbf7e61..7e3c4dab4588e 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml @@ -29,7 +29,7 @@ <!--Enable Advanced Setting--> <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> diff --git a/app/code/Magento/Persistent/Observer/EmulateCustomerObserver.php b/app/code/Magento/Persistent/Observer/EmulateCustomerObserver.php index 8429eabd19e8a..1ff81137de57b 100644 --- a/app/code/Magento/Persistent/Observer/EmulateCustomerObserver.php +++ b/app/code/Magento/Persistent/Observer/EmulateCustomerObserver.php @@ -6,6 +6,7 @@ namespace Magento\Persistent\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\NoSuchEntityException; /** * Class EmulateCustomer @@ -86,9 +87,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ $customer = $this->customerRepository->getById($this->_persistentSession->getSession()->getCustomerId()); if ($defaultShipping = $customer->getDefaultShipping()) { - /** @var \Magento\Customer\Model\Data\Address $address */ - $address = $this->addressRepository->getById($defaultShipping); - if ($address) { + $address = $this->getCustomerAddressById((int) $defaultShipping); + + if ($address !== null) { $this->_customerSession->setDefaultTaxShippingAddress( [ 'country_id' => $address->getCountryId(), @@ -102,8 +103,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) } if ($defaultBilling = $customer->getDefaultBilling()) { - $address = $this->addressRepository->getById($defaultBilling); - if ($address) { + $address = $this->getCustomerAddressById((int) $defaultBilling); + + if ($address !== null) { $this->_customerSession->setDefaultTaxBillingAddress([ 'country_id' => $address->getCountryId(), 'region_id' => $address->getRegion() ? $address->getRegionId() : null, @@ -118,4 +120,19 @@ public function execute(\Magento\Framework\Event\Observer $observer) } return $this; } + + /** + * Returns customer address by id + * + * @param int $addressId + * @return \Magento\Customer\Api\Data\AddressInterface|null + */ + private function getCustomerAddressById(int $addressId) + { + try { + return $this->addressRepository->getById($addressId); + } catch (NoSuchEntityException $exception) { + return null; + } + } } diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml index e6fae229d29b1..26db75d289a6e 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml @@ -45,6 +45,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> <createData entity="DisableSynchronizeWidgetProductsWithBackendStorage" stepKey="disableSynchronizeWidgetProductsWithBackendStorage"/> @@ -52,8 +53,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromCustomer"/> + <comment userInput="BIC workaround" stepKey="logoutFromCustomer"/> <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyComparedProductsWidget"> <argument name="widget" value="RecentlyComparedProductsWidget"/> </actionGroup> diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/EmulateCustomerObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/EmulateCustomerObserverTest.php index 6c35ade65451b..2df36577b2931 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/EmulateCustomerObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/EmulateCustomerObserverTest.php @@ -131,14 +131,14 @@ public function testExecuteWhenSessionPersistAndCustomerNotLoggedIn() $customerMock ->expects($this->once()) ->method('getDefaultShipping') - ->willReturn('shippingId'); + ->willReturn(12345); $customerMock ->expects($this->once()) ->method('getDefaultBilling') - ->willReturn('billingId'); + ->willReturn(12346); $valueMap = [ - ['shippingId', $defaultShippingAddressMock], - ['billingId', $defaultBillingAddressMock] + [12345, $defaultShippingAddressMock], + [12346, $defaultBillingAddressMock] ]; $this->addressRepositoryMock->expects($this->any())->method('getById')->willReturnMap($valueMap); $this->customerSessionMock diff --git a/app/code/Magento/ProductVideo/Model/ResourceModel/Video.php b/app/code/Magento/ProductVideo/Model/ResourceModel/Video.php index 68b593f335797..42fdf8265ee83 100644 --- a/app/code/Magento/ProductVideo/Model/ResourceModel/Video.php +++ b/app/code/Magento/ProductVideo/Model/ResourceModel/Video.php @@ -39,7 +39,8 @@ public function loadByIds(array $ids) $this->getMainTable() )->where( 'value_id IN(?)', - $ids + $ids, + \Zend_Db::INT_TYPE ); return $this->getConnection()->fetchAll($select); diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index d2e900138cd06..48cd4b2eb2fad 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -2312,7 +2312,9 @@ public function validateMinimumAmount($multishipping = false) if (!$minOrderMulti) { foreach ($addresses as $address) { - $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; + $taxes = $taxInclude + ? $address->getBaseTaxAmount() + $address->getBaseDiscountTaxCompensationAmount() + : 0; foreach ($address->getQuote()->getItemsCollection() as $item) { /** @var \Magento\Quote\Model\Quote\Item $item */ $amount = $includeDiscount ? @@ -2327,7 +2329,9 @@ public function validateMinimumAmount($multishipping = false) } else { $baseTotal = 0; foreach ($addresses as $address) { - $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; + $taxes = $taxInclude + ? $address->getBaseTaxAmount() + $address->getBaseDiscountTaxCompensationAmount() + : 0; $baseTotal += $includeDiscount ? $address->getBaseSubtotalWithDiscount() + $taxes : $address->getBaseSubtotal() + $taxes; diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index aee86eb1f8935..1ad98aa3e0263 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -139,7 +139,7 @@ class Address extends AbstractAddress implements const ADDRESS_TYPE_BILLING = 'billing'; const ADDRESS_TYPE_SHIPPING = 'shipping'; - + private const CACHED_ITEMS_ALL = 'cached_items_all'; /** @@ -1217,7 +1217,9 @@ public function validateMinimumAmount() $storeId ); - $taxes = $taxInclude ? $this->getBaseTaxAmount() : 0; + $taxes = $taxInclude + ? $this->getBaseTaxAmount() + $this->getBaseDiscountTaxCompensationAmount() + : 0; return $includeDiscount ? ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount) : @@ -1653,7 +1655,7 @@ public function setCustomerId($customerId) public function getEmail() { $email = $this->getData(self::KEY_EMAIL); - if (!$email && $this->getQuote()) { + if ($this->getQuote() && (!$email || $this->getQuote()->dataHasChangedFor('customer_email'))) { $email = $this->getQuote()->getCustomerEmail(); $this->setEmail($email); } diff --git a/app/code/Magento/Quote/Model/Quote/Plugin/UpdateQuoteStoreId.php b/app/code/Magento/Quote/Model/Quote/Plugin/UpdateQuoteStoreId.php new file mode 100644 index 0000000000000..bffa0084e35bd --- /dev/null +++ b/app/code/Magento/Quote/Model/Quote/Plugin/UpdateQuoteStoreId.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Quote\Plugin; + +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Updates quote store id. + */ +class UpdateQuoteStoreId +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreManagerInterface $storeManager + ) { + $this->storeManager = $storeManager; + } + + /** + * Update store id in requested quote by store id from request. + * + * @param Quote $subject + * @param Quote $result + * @return Quote + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterLoadByIdWithoutStore(Quote $subject, Quote $result): Quote + { + $storeId = $this->storeManager->getStore() + ->getId() ?: $this->storeManager->getDefaultStoreView() + ->getId(); + $result->setStoreId($storeId); + + return $result; + } +} diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml index a513b6747f612..d24154bcf9da2 100644 --- a/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml +++ b/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml @@ -23,4 +23,9 @@ <var key="quote_id" entityKey="return" entityType="GuestCart"/> <var key="sku" entityKey="sku" entityType="product"/> </entity> + <entity name="FourCartItems" type="CartItem"> + <data key="qty">4</data> + <var key="quote_id" entityKey="return" entityType="GuestCart"/> + <var key="sku" entityKey="sku" entityType="product"/> + </entity> </entities> diff --git a/app/code/Magento/Quote/etc/webapi_rest/di.xml b/app/code/Magento/Quote/etc/webapi_rest/di.xml index a55d2146be156..6ed9909f04eb9 100644 --- a/app/code/Magento/Quote/etc/webapi_rest/di.xml +++ b/app/code/Magento/Quote/etc/webapi_rest/di.xml @@ -16,4 +16,7 @@ <type name="Magento\Quote\Api\GuestCartItemRepositoryInterface"> <plugin name="updateCartIdFromRequest" type="Magento\Quote\Plugin\UpdateCartId" /> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="updateQuoteStoreId" type="Magento\Quote\Model\Quote\Plugin\UpdateQuoteStoreId" /> + </type> </config> diff --git a/app/code/Magento/Quote/etc/webapi_soap/di.xml b/app/code/Magento/Quote/etc/webapi_soap/di.xml index 27d5ff7753425..4b7646b6e1ef3 100644 --- a/app/code/Magento/Quote/etc/webapi_soap/di.xml +++ b/app/code/Magento/Quote/etc/webapi_soap/di.xml @@ -13,4 +13,7 @@ <plugin name="accessControl" type="Magento\Quote\Model\QuoteRepository\Plugin\AccessChangeQuoteControl" /> <plugin name="authorization" type="Magento\Quote\Model\QuoteRepository\Plugin\Authorization" /> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="updateQuoteStoreId" type="Magento\Quote\Model\Quote\Plugin\UpdateQuoteStoreId" /> + </type> </config> diff --git a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php index b6e55af96f4c1..a346ad4ede29b 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php @@ -213,7 +213,8 @@ protected function _addOrdersStatistics() \Magento\Sales\Model\Order::STATE_CANCELED )->where( 'orders.customer_id IN(?)', - $customerIds + $customerIds, + \Zend_Db::INT_TYPE )->group( 'orders.customer_id' ); diff --git a/app/code/Magento/Review/Ui/DataProvider/Product/ReviewDataProvider.php b/app/code/Magento/Review/Ui/DataProvider/Product/ReviewDataProvider.php index a9c011d4a4865..315739e94fc0c 100644 --- a/app/code/Magento/Review/Ui/DataProvider/Product/ReviewDataProvider.php +++ b/app/code/Magento/Review/Ui/DataProvider/Product/ReviewDataProvider.php @@ -5,14 +5,14 @@ */ namespace Magento\Review\Ui\DataProvider\Product; +use Magento\Framework\Api\Filter; use Magento\Framework\App\RequestInterface; -use Magento\Ui\DataProvider\AbstractDataProvider; -use Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory; use Magento\Review\Model\ResourceModel\Review\Product\Collection; -use Magento\Review\Model\Review; +use Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory; +use Magento\Ui\DataProvider\AbstractDataProvider; /** - * Class ReviewDataProvider + * DataProvider for product reviews * * @api * @@ -58,7 +58,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getData() @@ -79,24 +79,42 @@ public function getData() } /** - * {@inheritdoc} - * @since 100.1.0 + * Returns prepared field name + * + * @param string $name + * @return string */ - public function addFilter(\Magento\Framework\Api\Filter $filter) + private function getPreparedField(string $name): string { - $field = $filter->getField(); + $preparedName = ''; - if (in_array($field, ['review_id', 'created_at', 'status_id'])) { - $filter->setField('rt.' . $field); + if (in_array($name, ['review_id', 'created_at', 'status_id'])) { + $preparedName = 'rt.' . $name; + } elseif (in_array($name, ['title', 'nickname', 'detail'])) { + $preparedName = 'rdt.' . $name; + } elseif ($name === 'review_created_at') { + $preparedName = 'rt.created_at'; } - if (in_array($field, ['title', 'nickname', 'detail'])) { - $filter->setField('rdt.' . $field); - } + return $preparedName ?: $name; + } - if ($field === 'review_created_at') { - $filter->setField('rt.created_at'); - } + /** + * @inheritDoc + */ + public function addOrder($field, $direction) + { + $this->getCollection()->setOrder($this->getPreparedField($field), $direction); + } + + /** + * @inheritdoc + * @since 100.1.0 + */ + public function addFilter(Filter $filter) + { + $field = $filter->getField(); + $filter->setField($this->getPreparedField($field)); parent::addFilter($filter); } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php index 3d1160c0ca4f2..63558c0290e2c 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Sales\Helper\Data as SalesData; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; @@ -34,21 +35,30 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac */ protected $resultForwardFactory; + /** + * @var SalesData + */ + private $salesData; + /** * @param Action\Context $context * @param \Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader $creditmemoLoader * @param CreditmemoSender $creditmemoSender * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param SalesData $salesData */ public function __construct( Action\Context $context, \Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader $creditmemoLoader, CreditmemoSender $creditmemoSender, - \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, + SalesData $salesData = null ) { $this->creditmemoLoader = $creditmemoLoader; $this->creditmemoSender = $creditmemoSender; $this->resultForwardFactory = $resultForwardFactory; + $this->salesData = $salesData ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(SalesData::class); parent::__construct($context); } @@ -108,7 +118,7 @@ public function execute() $doOffline = isset($data['do_offline']) ? (bool)$data['do_offline'] : false; $creditmemoManagement->refund($creditmemo, $doOffline); - if (!empty($data['send_email'])) { + if (!empty($data['send_email']) && $this->salesData->canSendNewCreditMemoEmail()) { $this->creditmemoSender->send($creditmemo); } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php index f66ca37a47655..ae3c1af1e3195 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php @@ -86,7 +86,8 @@ public function __construct( $this->shipmentFactory = $shipmentFactory; $this->invoiceService = $invoiceService; parent::__construct($context); - $this->salesData = $salesData ?? $this->_objectManager->get(SalesData::class); + $this->salesData = $salesData ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(SalesData::class); } /** @@ -213,7 +214,7 @@ public function execute() // send invoice/shipment emails try { - if (!empty($data['send_email']) || $this->salesData->canSendNewInvoiceEmail()) { + if (!empty($data['send_email']) && $this->salesData->canSendNewInvoiceEmail()) { $this->invoiceSender->send($invoice); } } catch (\Exception $e) { @@ -222,7 +223,7 @@ public function execute() } if ($shipment) { try { - if (!empty($data['send_email']) || $this->salesData->canSendNewShipmentEmail()) { + if (!empty($data['send_email']) && $this->salesData->canSendNewShipmentEmail()) { $this->shipmentSender->send($shipment); } } catch (\Exception $e) { diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 3e708d1a0df02..5d621f1632837 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -984,6 +984,7 @@ public function applySidebarData($data) ); if ($item->getId()) { $this->addProduct($item->getProduct(), $item->getBuyRequest()->toArray()); + $this->removeItem($itemId, 'wishlist'); } } } diff --git a/app/code/Magento/Sales/Model/EmailSenderHandler.php b/app/code/Magento/Sales/Model/EmailSenderHandler.php index a201c1285ae49..3a7a5727d8341 100644 --- a/app/code/Magento/Sales/Model/EmailSenderHandler.php +++ b/app/code/Magento/Sales/Model/EmailSenderHandler.php @@ -132,8 +132,9 @@ public function sendEmails() /** @var \Magento\Sales\Model\AbstractModel $item */ foreach ($entityCollection->getItems() as $item) { if ($this->emailSender->send($item, true)) { - $this->entityResource->save( - $item->setEmailSent(true) + $this->entityResource->saveAttribute( + $item->setEmailSent(true), + 'email_sent' ); } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddSimpleProductToOrderAndCheckCheckboxActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddSimpleProductToOrderAndCheckCheckboxActionGroup.xml new file mode 100644 index 0000000000000..67b10d6955775 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddSimpleProductToOrderAndCheckCheckboxActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddSimpleProductToOrderAndCheckCheckboxActionGroup" extends="AddSimpleProductToOrderActionGroup"> + <annotations> + <description>Adds the provided Simple Product to an Order. Checks if checkbox is checked. Fills in the provided Product Qty. Clicks on 'Add Selected Product(s) to Order'.</description> + </annotations> + + <seeCheckboxIsChecked selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="verifyProductChecked" after="selectProduct"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminFillAccountInformationOnCreateOrderPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminFillAccountInformationOnCreateOrderPageActionGroup.xml new file mode 100644 index 0000000000000..acf4ff8b43eca --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminFillAccountInformationOnCreateOrderPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillAccountInformationOnCreateOrderPageActionGroup"> + <arguments> + <argument name="group" defaultValue="{{GeneralCustomerGroup.code}}" type="string"/> + <argument name="email" type="string"/> + </arguments> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{group}}" stepKey="selectCustomerGroup"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{email}}" stepKey="fillCustomerEmail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceWithUpdatedProductQtyActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceWithUpdatedProductQtyActionGroup.xml new file mode 100644 index 0000000000000..0f602c42ade25 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceWithUpdatedProductQtyActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminInvoiceWithUpdatedProductQtyActionGroup" extends="AdminCreateInvoiceActionGroup"> + <annotations> + <description>Start order Invoicing. + Update product qty to invoice (there is one product in the Order). + Submit the invoice. + </description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + + <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="{{qty}}" stepKey="fillQtyField" after="waitForInvoicePage"/> + <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="clickUpdateQuantityButton" after="fillQtyField"/> + <waitForPageLoad stepKey="waitForPageRefreshed" after="clickUpdateQuantityButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/CreditMemoData.xml b/app/code/Magento/Sales/Test/Mftf/Data/CreditMemoData.xml new file mode 100644 index 0000000000000..dc6280f7b3444 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/CreditMemoData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + + <entity name="CreditMemo" type="CreditMemo"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + </entity> + +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/InvoiceData.xml b/app/code/Magento/Sales/Test/Mftf/Data/InvoiceData.xml new file mode 100644 index 0000000000000..b55ef96932100 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/InvoiceData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + + <entity name="Invoice" type="Invoice"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + </entity> + +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/ShipmentData.xml b/app/code/Magento/Sales/Test/Mftf/Data/ShipmentData.xml new file mode 100644 index 0000000000000..96d371d3aaf3a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/ShipmentData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + + <entity name="Shipment" type="Shipment"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + </entity> + +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/CreditMemoMeta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/CreditMemoMeta.xml new file mode 100644 index 0000000000000..b051c91ba3b10 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Metadata/CreditMemoMeta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCreditMemo" dataType="CreditMemo" type="create" auth="adminOauth" url="V1/order/{return}/refund" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CartItem"> + <field key="quote_id">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/InvoiceMeta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/InvoiceMeta.xml new file mode 100644 index 0000000000000..e0bbfa1cd6e5f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Metadata/InvoiceMeta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateInvoice" dataType="Invoice" type="create" auth="adminOauth" url="V1/order/{return}/invoice" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CartItem"> + <field key="quote_id">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/ShipmentMeta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/ShipmentMeta.xml new file mode 100644 index 0000000000000..1c9de02c6a2e7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Metadata/ShipmentMeta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateShipment" dataType="Shipment" type="create" auth="adminOauth" url="V1/order/{return}/ship" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CartItem"> + <field key="quote_id">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormCustomOptionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormCustomOptionsSection.xml index 066aa4181e7ef..49d26d4973874 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormCustomOptionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormCustomOptionsSection.xml @@ -10,7 +10,7 @@ <section name="AdminOrderFormCustomOptionsSection"> <element name="quantity" type="input" selector="//input[@id='product_composite_configure_input_qty']"/> <element name="file" type="file" selector="//input[@type='file'][contains(@class, 'product-custom-option')]" /> - <element name="buttonOk" type="button" selector="//button[contains(@class, 'action-primary')][@data-role='action']"/> + <element name="buttonOk" type="button" selector="//button[contains(@class, 'action-primary')][@data-role='action']" timeout="30"/> <element name="linkChange" type="text" selector="//div[contains(@class, 'entry-edit')]//a[contains(text(),'Change')]"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml index 4437f6e6775f2..fae0bd4589580 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml @@ -13,7 +13,7 @@ <element name="configureButtonBySku" type="button" selector="//div[@class='sku-configure-button']//span[contains(text(),'Configure')]"/> <element name="configureProductOk" type="button" selector="//div[@class='page-main-actions']//span[contains(text(),'OK')]"/> <element name="configureProductQtyField" type="input" selector="//*[@id='super-product-table']/tbody/tr[{{arg}}]/td[5]/input[1]" parameterized="true"/> - <element name="addProductToOrder" type="input" selector="//*[@title='Add Products to Order']"/> + <element name="addProductToOrder" type="input" selector="//*[@title='Add Products to Order']" timeout="30"/> <element name="itemsOrderedSummaryText" type="textarea" selector="//table[@class='data-table admin__table-primary order-tables']/tfoot/tr"/> <element name="configureSelectAttribute" type="select" selector="select[id*=attribute]"/> <element name="itemsSKU" type="text" selector="(//div[contains(@class, 'product-sku-block')])[{{productNumber}}]" parameterized="true"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index 02878e79f3d70..55cd30f1caf2e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -41,5 +41,6 @@ <element name="viewLink" type="text" selector="//td/div[contains(.,'{{orderID}}')]/../..//a[@class='action-menu-item']" parameterized="true"/> <element name="selectOrderID" type="checkbox" selector="//td/div[text()='{{orderId}}']/../preceding-sibling::td//input" parameterized="true" timeout="60"/> <element name="orderId" type="text" selector="//table[contains(@class, 'data-grid')]//div[contains(text(), '{{orderId}}')]" parameterized="true"/> + <element name="orderIdByIncrementId" type="text" selector="//input[@class='admin__control-checkbox' and @value={{incrId}}]/parent::label/parent::td/following-sibling::td" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml index d1c94965640c5..12beba19b4375 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml @@ -14,6 +14,8 @@ <element name="billingAddressBlock" type="block" selector=".box-order-billing-address > .box-content > address"/> <element name="discountSalesRule" type="text" selector="tr.discount span.price"/> <element name="shippingTotalDescription" type="text" selector="#my-orders-table tr.shipping th.mark"/> + <element name="tax" type="text" selector=".totals-tax .price"/> + <element name="grandTotalIncludingTax" type="text" selector=".grand_total_incl .amount"/> <element name="grandTotalPrice" type="text" selector="tr.grand_total span.price"/> <element name="paymentMethod" type="text" selector=".box-order-billing-method dt.title"/> <element name="shippingMethod" type="text" selector=".box-order-shipping-method div.box-content"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml index 182549a6fe301..afede97556513 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -61,8 +61,10 @@ <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="clickUpdateItemsAndQuantitiesButton"/> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml index 6ce9909d06be5..21ced2e2df278 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml @@ -35,7 +35,7 @@ <field key="price">10.00</field> </createData> - <!-- Create a slaes rule with fixed discount --> + <!-- Create a sales rule with fixed discount --> <createData entity="SalesRuleNoCouponWithFixedDiscount" stepKey="createSalesRule"/> </before> <after> @@ -47,7 +47,6 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!--Create new customer order--> <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> @@ -62,10 +61,20 @@ <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> + <!-- Start the consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{SalesRuleConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{SalesRuleConsumerData.messageLimit}}"/> + </actionGroup> + <!--Verify order information--> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> + <reloadPage stepKey="refreshPage"/> <grabTextFrom selector="|Order # (\d+)|" stepKey="orderId"/> + <!-- Refresh the page --> + <reloadPage stepKey="refreshPageAgain"/> + <!-- Cancel the Order --> <actionGroup ref="CancelPendingOrderActionGroup" stepKey="cancelPendingOrder"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml index baef605ad52af..13b91fa605bca 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml @@ -34,17 +34,20 @@ <argument name="customer" value="$$createSimpleCustomer$$"/> </actionGroup> - <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> - <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSkuFilterBundle"/> - <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchBundle"/> - <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> - <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectProduct"/> - <seeCheckboxIsChecked selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="verifyProductChecked"/> - + <actionGroup ref="AdminAddSimpleProductToOrderAndCheckCheckboxActionGroup" stepKey="clickAddProducts"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="fillSkuFilterBundle"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickSearchBundle"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="scrollToCheckColumn"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectProduct"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="verifyProductChecked"/> + <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createSimpleCustomer" stepKey="deleteSimpleCustomer"/> </after> </test> -</tests> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml index b5c9e9443d1f9..6b714e0d18726 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -45,9 +45,11 @@ </actionGroup> <!--Fill customer group information--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectGroup"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillEmail"/> - + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectGroup"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillEmail"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress"> <argument name="customer" value="Simple_US_Customer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml new file mode 100644 index 0000000000000..794d09226d87f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMassOrdersCancelClosedAndCompleteTest"> + <annotations> + <stories value="Mass Update Orders"/> + <title value="Mass cancel orders in status Complete, Closed"/> + <description value="Try to cancel orders in status Complete, Closed"/> + <severity value="MAJOR"/> + <testCaseId value="MC-39905"/> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + + <createData entity="ApiCategory" stepKey="createCategory"/> + + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="GuestCart" stepKey="createGuestCartOne"/> + <createData entity="SimpleCartItem" stepKey="addCartItemOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddressOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + <updateData createDataKey="createGuestCartOne" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformationOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </updateData> + + <createData entity="Invoice" stepKey="invoiceOrderOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + + <createData entity="Shipment" stepKey="shipOrderOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + + <createData entity="GuestCart" stepKey="createGuestCartTwo"/> + <createData entity="SimpleCartItem" stepKey="addCartItemTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddressTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + <updateData createDataKey="createGuestCartTwo" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformationTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </updateData> + + <createData entity="Invoice" stepKey="invoiceOrderTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + + <createData entity="CreditMemo" stepKey="refundOrderTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> + + <grabTextFrom selector="{{AdminOrdersGridSection.orderIdByIncrementId($createGuestCartOne.return$)}}" stepKey="getOrderOneId"/> + <grabTextFrom selector="{{AdminOrdersGridSection.orderIdByIncrementId($createGuestCartTwo.return$)}}" stepKey="getOrderTwoId"/> + + <actionGroup ref="AdminTwoOrderActionOnGridActionGroup" stepKey="massActionCancel"> + <argument name="action" value="Cancel"/> + <argument name="orderId" value="$getOrderOneId"/> + <argument name="secondOrderId" value="$getOrderTwoId"/> + </actionGroup> + <see userInput="You cannot cancel the order(s)." stepKey="assertOrderCancelMassActionFailMessage"/> + + <actionGroup ref="AdminOrderFilterByOrderIdAndStatusActionGroup" stepKey="seeFirstOrder"> + <argument name="orderId" value="$getOrderOneId"/> + <argument name="orderStatus" value="Complete"/> + </actionGroup> + <see userInput="$getOrderOneId" selector="{{AdminOrdersGridSection.gridCell('1','ID')}}" stepKey="assertFirstOrderID"/> + <see userInput="Complete" selector="{{AdminOrdersGridSection.gridCell('1','Status')}}" stepKey="assertFirstOrderStatus"/> + + <actionGroup ref="AdminOrderFilterByOrderIdAndStatusActionGroup" stepKey="seeSecondOrder"> + <argument name="orderId" value="$getOrderTwoId"/> + <argument name="orderStatus" value="Closed"/> + </actionGroup> + <see userInput="$getOrderTwoId" selector="{{AdminOrdersGridSection.gridCell('1','ID')}}" stepKey="assertSecondOrderID"/> + <see userInput="Closed" selector="{{AdminOrdersGridSection.gridCell('1','Status')}}" stepKey="assertSecondStatus"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml index b337af3753db3..10b3ba05aa2bb 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml @@ -8,15 +8,18 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMassOrdersCancelCompleteAndClosedTest"> + <test name="AdminMassOrdersCancelCompleteAndClosedTest" deprecated="Use AdminMassOrdersCancelClosedAndCompleteTest instead"> <annotations> <stories value="Mass Update Orders"/> - <title value="Mass cancel orders in status Complete, Closed"/> + <title value="DEPRECATED. Mass cancel orders in status Complete, Closed"/> <description value="Try to cancel orders in status Complete, Closed"/> <severity value="MAJOR"/> <testCaseId value="MC-16183"/> <group value="sales"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use AdminMassOrdersCancelClosedAndCompleteTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml index ac0af3f5b80db..f8136a9071a1a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml @@ -47,6 +47,8 @@ <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> + <!-- By default checkbox 'Add to address book' must be unchecked --> + <dontSeeCheckboxIsChecked selector="{{AdminOrderFormBillingAddressSection.SaveAddress}}" stepKey="checkBoxAddBillingAddressIsUnchecked"/> <!-- Just in case uncheck and check 'Same as Billing Address checkbox' --> <comment userInput="Just in case uncheck and check 'Same as Billing Address checkbox'" stepKey="uncheckAndCheckAgain"/> <uncheckOption selector="{{AdminOrderFormShippingAddressSection.SameAsBilling}}" stepKey="unCheckSameAsShippingAddressCheckbox"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml index bd6a21e3112ca..addf978235af4 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml @@ -48,9 +48,11 @@ <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="seePaymentMethodRequired"/> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="scrollToTopOfOrderFormPage"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> - + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup" after="scrollToTopOfOrderFormPage"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail" after="selectCustomerGroup"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress" after="fillCustomerEmail"> <argument name="customer" value="Simple_US_Customer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index 727aef99352ec..7c2309eeb7be3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -46,8 +46,10 @@ </actionGroup> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail" after="selectCustomerGroup"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress" after="fillCustomerEmail"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml index 2bedb16f3d1dc..db779a7340b07 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -45,8 +45,10 @@ </actionGroup> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail" after="selectCustomerGroup"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> <!--Fill wrong customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillWrongCustomerAddress" after="fillCustomerEmail"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml index 367c50359701c..08d5776b79ea0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml @@ -136,14 +136,15 @@ <see selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$simpleProduct.name$$" stepKey="seeSimpleProductInWishList"/> <see selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProductInWishList"/> - <!-- Add products to order from Wish List --> + <!-- Move products to order from Wish List --> <waitForElementVisible selector="{{AdminCreateOrderWishListSection.addProductToOrderCheckBox($$simpleProduct.name$$)}}" stepKey="waitForCheckBoxToVisible"/> <click selector="{{AdminCreateOrderWishListSection.addProductToOrderCheckBox($$simpleProduct.name$$)}}" stepKey="selectProductToAddToOrder"/> + <click selector="{{AdminCustomerCreateNewOrderSection.updateChangesBtn}}" stepKey="clickUpdateChangesButton"/> <click selector="{{AdminCreateOrderWishListSection.addConfigProductToOrder($$createConfigProduct.name$$)}}" stepKey="AddConfigurableProductToOrder"/> <waitForElementVisible selector="{{AdminOrderFormConfigureProductSection.optionSelect($$createConfigProductAttribute.default_frontend_label$$)}}" stepKey="waitForConfigurablePopover"/> <selectOption selector="{{AdminOrderFormConfigureProductSection.optionSelect($$createConfigProductAttribute.default_frontend_label$$)}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="selectConfigurableOption"/> <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOkButton"/> - <click selector="{{AdminCustomerCreateNewOrderSection.updateChangesBtn}}" stepKey="clickOnUpdateButton"/> + <comment userInput="Action should be removed but replaced with comment due to backward compatibility" stepKey="clickOnUpdateButton"/> <waitForPageLoad stepKey="waitForAdminOrderItemsOrderedSectionPageLoad1"/> <!-- Assert Products in Order item section --> @@ -171,9 +172,9 @@ <dontSee selector="{{AdminCreateOrderShoppingCartSection.shoppingCartBlock}}" userInput="$$simpleProduct.name$$" stepKey="donSeeProductInShoppingCart"/> <dontSee selector="{{AdminCreateOrderShoppingCartSection.shoppingCartBlock}}" userInput="$$simpleProduct1.name$$" stepKey="dontSeeSecondProductInShoppingCart"/> - <!-- After move, assert products are present in Wish List section --> - <see selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$simpleProduct.name$$" stepKey="seeSimpleProductInWishList1"/> - <see selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProductInWishList1"/> + <!-- After move, assert products are not present in Wish List section --> + <dontSee selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$simpleProduct.name$$" stepKey="seeSimpleProductInWishList1"/> + <dontSee selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProductInWishList1"/> <!-- After move, assert products are present in order items section --> <see selector="{{AdminOrderItemsOrderedSection.productName}}" userInput="$$simpleProduct.name$$" stepKey="seeSimpleProductInOrderItemGrid1"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml index 0718783534925..2b60c1d7ba550 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml @@ -24,9 +24,8 @@ </createData> <!-- Create Customer Account --> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <!-- Reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'indexer:reindex' command for preserving Backward Compatibility" stepKey="reindex"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php index 8ccb821f6399f..fef38f981e12d 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php @@ -21,9 +21,13 @@ use Magento\Framework\Registry; use Magento\Framework\Session\Storage; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\CreditmemoManagementInterface; use Magento\Sales\Controller\Adminhtml\Order\Creditmemo\Save; use Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader; +use Magento\Sales\Helper\Data as SalesData; +use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Creditmemo; +use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -87,6 +91,16 @@ class SaveTest extends TestCase */ protected $resultRedirectMock; + /** + * @var CreditmemoSender|MockObject + */ + private $creditmemoSender; + + /** + * @var SalesData|MockObject + */ + private $salesData; + /** * Init model for future tests */ @@ -147,12 +161,32 @@ protected function setUp(): void $context = $helper->getObject(Context::class, $arguments); + $creditmemoManagement = $this->getMockBuilder(CreditmemoManagementInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->_objectManager->expects($this->any()) + ->method('create') + ->with(CreditmemoManagementInterface::class) + ->willReturn($creditmemoManagement); + $this->creditmemoSender = $this->getMockBuilder(CreditMemoSender::class) + ->disableOriginalConstructor() + ->setMethods(['send']) + ->getMock(); + $this->creditmemoSender->expects($this->any()) + ->method('send') + ->willReturn(true); + $this->salesData = $this->getMockBuilder(SalesData::class) + ->disableOriginalConstructor() + ->setMethods(['canSendNewCreditmemoEmail']) + ->getMock(); $this->memoLoaderMock = $this->createMock(CreditmemoLoader::class); $this->_controller = $helper->getObject( Save::class, [ 'context' => $context, 'creditmemoLoader' => $this->memoLoaderMock, + 'creditmemoSender' => $this->creditmemoSender, + 'salesData' => $this->salesData ] ); } @@ -258,4 +292,94 @@ protected function _setSaveActionExpectationForMageCoreException($data, $errorMe $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($errorMessage); $this->_sessionMock->expects($this->once())->method('setFormData')->with($data); } + + /** + * @return array + */ + public function testExecuteEmailsDataProvider() + { + /** + * string $sendEmail + * bool $emailEnabled + * bool $shouldEmailBeSent + */ + return [ + ['', false, false], + ['', true, false], + ['on', false, false], + ['on', true, true] + ]; + } + + /** + * @param string $sendEmail + * @param bool $emailEnabled + * @param bool $shouldEmailBeSent + * @dataProvider testExecuteEmailsDataProvider + */ + public function testExecuteEmails( + $sendEmail, + $emailEnabled, + $shouldEmailBeSent + ) { + $orderId = 1; + $creditmemoId = 2; + $invoiceId = 3; + $creditmemoData = ['items' => [], 'send_email' => $sendEmail]; + + $this->resultRedirectFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultRedirectMock); + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('sales/order/view', ['order_id' => $orderId]) + ->willReturnSelf(); + + $order = $this->createPartialMock( + Order::class, + [] + ); + + $creditmemo = $this->createPartialMock( + Creditmemo::class, + ['isValidGrandTotal', 'getOrder', 'getOrderId'] + ); + $creditmemo->expects($this->once()) + ->method('isValidGrandTotal') + ->willReturn(true); + $creditmemo->expects($this->once()) + ->method('getOrder') + ->willReturn($order); + $creditmemo->expects($this->once()) + ->method('getOrderId') + ->willReturn($orderId); + + $this->_requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['order_id', null, $orderId], + ['creditmemo_id', null, $creditmemoId], + ['creditmemo', null, $creditmemoData], + ['invoice_id', null, $invoiceId] + ] + ); + + $this->_requestMock->expects($this->any()) + ->method('getPost') + ->willReturn($creditmemoData); + + $this->memoLoaderMock->expects($this->once()) + ->method('load') + ->willReturn($creditmemo); + + $this->salesData->expects($this->any()) + ->method('canSendNewCreditmemoEmail') + ->willReturn($emailEnabled); + if ($shouldEmailBeSent) { + $this->creditmemoSender->expects($this->once()) + ->method('send'); + } + $this->assertEquals($this->resultRedirectMock, $this->_controller->execute()); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/SaveTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/SaveTest.php index a84eee24e2e99..4029cd8368343 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/SaveTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/SaveTest.php @@ -8,16 +8,27 @@ namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order\Invoice; use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\Session; use Magento\Backend\Model\View\Result\Redirect; use Magento\Framework\App\Request\Http; use Magento\Framework\Data\Form\FormKey\Validator; +use Magento\Framework\DB\Transaction; use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\ObjectManager\ObjectManager as FrameworkObjectManager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Result\PageFactory; use Magento\Sales\Controller\Adminhtml\Order\Invoice\Save; +use Magento\Sales\Helper\Data as SalesData; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Service\InvoiceService; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveTest extends TestCase { /** @@ -50,6 +61,26 @@ class SaveTest extends TestCase */ protected $controller; + /** + * @var SalesData|MockObject + */ + private $salesData; + + /** + * @var InvoiceSender|MockObject + */ + private $invoiceSender; + + /** + * @var FrameworkObjectManager|MockObject + */ + private $objectManager; + + /** + * @var InvoiceService|MockObject + */ + private $invoiceService; + /** * SetUp method * @@ -98,11 +129,36 @@ protected function setUp(): void $contextMock->expects($this->any()) ->method('getMessageManager') ->willReturn($this->messageManagerMock); + $this->objectManager = $this->createPartialMock( + FrameworkObjectManager::class, + ['create','get'] + ); + $contextMock->expects($this->any()) + ->method('getObjectManager') + ->willReturn($this->objectManager); + $this->invoiceSender = $this->getMockBuilder(InvoiceSender::class) + ->disableOriginalConstructor() + ->setMethods(['send']) + ->getMock(); + $this->invoiceSender->expects($this->any()) + ->method('send') + ->willReturn(true); + $this->salesData = $this->getMockBuilder(SalesData::class) + ->disableOriginalConstructor() + ->setMethods(['canSendNewInvoiceEmail']) + ->getMock(); + $this->invoiceService = $this->getMockBuilder(InvoiceService::class) + ->disableOriginalConstructor() + ->setMethods(['prepareInvoice']) + ->getMock(); $this->controller = $objectManager->getObject( Save::class, [ 'context' => $contextMock, + 'invoiceSender' => $this->invoiceSender, + 'invoiceService' => $this->invoiceService, + 'salesData' => $this->salesData ] ); } @@ -137,4 +193,148 @@ public function testExecuteNotValidPost() $this->assertEquals($redirectMock, $this->controller->execute()); } + + /** + * @return array + */ + public function testExecuteEmailsDataProvider() + { + /** + * string $sendEmail + * bool $emailEnabled + * bool $shouldEmailBeSent + */ + return [ + ['', false, false], + ['', true, false], + ['on', false, false], + ['on', true, true] + ]; + } + + /** + * @param string $sendEmail + * @param bool $emailEnabled + * @param bool $shouldEmailBeSent + * @dataProvider testExecuteEmailsDataProvider + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testExecuteEmails( + $sendEmail, + $emailEnabled, + $shouldEmailBeSent + ) { + $redirectMock = $this->getMockBuilder(Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + $redirectMock->expects($this->once()) + ->method('setPath') + ->with('sales/order/view') + ->willReturnSelf(); + + $this->resultPageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($redirectMock); + $this->formKeyValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->requestMock) + ->willReturn(true); + $this->requestMock->expects($this->once()) + ->method('isPost') + ->willReturn(true); + + $invoiceData = ['items' => [], 'send_email' => $sendEmail]; + + $orderId = 2; + $order = $this->createPartialMock( + Order::class, + ['load','getId','canInvoice'] + ); + $order->expects($this->once()) + ->method('load') + ->willReturn($order); + $order->expects($this->once()) + ->method('getId') + ->willReturn($orderId); + $order->expects($this->once()) + ->method('canInvoice') + ->willReturn(true); + + $invoice = $this->getMockBuilder(Invoice::class) + ->disableOriginalConstructor() + ->setMethods(['getTotalQty','getOrder','register']) + ->getMock(); + $invoice->expects($this->any()) + ->method('getTotalQty') + ->willReturn(1); + $invoice->expects($this->any()) + ->method('getOrder') + ->willReturn($order); + $invoice->expects($this->once()) + ->method('register') + ->willReturn($order); + + $this->invoiceService->expects($this->any()) + ->method('prepareInvoice') + ->willReturn($invoice); + + $saveTransaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->setMethods(['addObject','save']) + ->getMock(); + $saveTransaction->expects($this->at(0)) + ->method('addObject') + ->with($invoice)->willReturnSelf(); + $saveTransaction->expects($this->at(1)) + ->method('addObject') + ->with($order)->willReturnSelf(); + $saveTransaction->expects($this->at(2)) + ->method('save'); + + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getCommentText']) + ->getMock(); + $session->expects($this->once()) + ->method('getCommentText') + ->with(true); + + $this->objectManager->expects($this->any()) + ->method('create') + ->will( + $this->returnValueMap( + [ + [Transaction::class, [], $saveTransaction], + [Order::class, [], $order], + [Session::class, [], $session] + ] + ) + ); + $this->objectManager->expects($this->any()) + ->method('get') + ->with(Session::class) + ->willReturn($session); + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['order_id', null, $orderId], + ['invoice', null, $invoiceData] + ] + ); + $this->requestMock->expects($this->any()) + ->method('getPost') + ->willReturn($invoiceData); + + $this->salesData->expects($this->any()) + ->method('canSendNewInvoiceEmail') + ->willReturn($emailEnabled); + if ($shouldEmailBeSent) { + $this->invoiceSender->expects($this->once()) + ->method('send'); + } + + $this->assertEquals($redirectMock, $this->controller->execute()); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php b/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php index 757a026aa5d68..2a7b44efa5261 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php @@ -100,7 +100,7 @@ protected function setUp(): void false, false, true, - ['save'] + ['saveAttribute'] ); $this->entityCollection = $this->getMockForAbstractClass( @@ -252,7 +252,7 @@ public function testExecute($configValue, $collectionItems, $emailSendingResult) $this->entityResource ->expects($this->once()) - ->method('save') + ->method('saveAttribute') ->with($collectionItem); } } diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index 0e65131b7c4b0..13afa0832086e 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -804,3 +804,4 @@ If set YES Email field will be required during Admin order creation for new Cust "Please enter a coupon code!","Please enter a coupon code!" "Reorder is not available.","Reorder is not available." "The coupon code has been removed.","The coupon code has been removed." +"Add to address book","Add to address book" diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 80083569df889..638ac7e66f769 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -125,7 +125,7 @@ endif; ?> <?php endif; ?> class="admin__control-checkbox"/> <label for="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" - class="admin__field-label"><?= $block->escapeHtml(__('Save in address book')) ?></label> + class="admin__field-label"><?= $block->escapeHtml(__('Add to address book')) ?></label> </div> </div> <?php $hideElement = 'address-' . ($block->getIsShipping() ? 'shipping' : 'billing') . '-overlay'; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index ba93f5f88c387..9454fe17fe2d2 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -942,7 +942,8 @@ define([ */ sidebarConfigureProduct: function (listType, productId, itemId) { // create additional fields - var params = {}; + var params = {}, + isWishlist = !!itemId; params.reset_shipping = true; params.add_product = productId; this.prepareParams(params); @@ -963,10 +964,18 @@ define([ }.bind(this)); // response handler productConfigure.setOnLoadIFrameCallback(listType, function (response) { + var areas = ['items', 'shipping_method', 'billing_method', 'totals', 'giftmessage']; + if (!response.ok) { return; } - this.loadArea(['items', 'shipping_method', 'billing_method', 'totals', 'giftmessage'], true); + if (isWishlist) { + this.removeSidebarItem(itemId, 'wishlist').done(function () { + this.loadArea(areas, true); + }.bind(this)); + } else { + this.loadArea(areas, true); + } }.bind(this)); // show item configuration itemId = itemId ? itemId : productId; @@ -975,7 +984,10 @@ define([ }, removeSidebarItem: function (id, from) { - this.loadArea(['sidebar_' + from], 'sidebar_data_' + from, {remove_item: id, from: from}); + return this.loadArea(['sidebar_' + from], 'sidebar_data_' + from, { + remove_item: id, + from: from + }); }, itemsUpdate: function () { diff --git a/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php index 0ee2ee09cad57..02da921e032e0 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php +++ b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php @@ -8,9 +8,9 @@ namespace Magento\SalesRule\Model\Coupon\Quote; use Magento\Quote\Api\Data\CartInterface; -use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; +use Magento\SalesRule\Model\Service\CouponUsagePublisher; /** * Updates the coupon usages from quote @@ -18,24 +18,24 @@ class UpdateCouponUsages { /** - * @var CouponUsageProcessor + * @var UpdateInfoFactory */ - private $couponUsageProcessor; + private $updateInfoFactory; /** - * @var UpdateInfoFactory + * @var CouponUsagePublisher */ - private $updateInfoFactory; + private $couponUsagePublisher; /** - * @param CouponUsageProcessor $couponUsageProcessor + * @param CouponUsagePublisher $couponUsagePublisher * @param UpdateInfoFactory $updateInfoFactory */ public function __construct( - CouponUsageProcessor $couponUsageProcessor, + CouponUsagePublisher $couponUsagePublisher, UpdateInfoFactory $updateInfoFactory ) { - $this->couponUsageProcessor = $couponUsageProcessor; + $this->couponUsagePublisher = $couponUsagePublisher; $this->updateInfoFactory = $updateInfoFactory; } @@ -59,6 +59,6 @@ public function execute(CartInterface $quote, bool $increment): void $updateInfo->setCustomerId((int)$quote->getCustomerId()); $updateInfo->setIsIncrement($increment); - $this->couponUsageProcessor->process($updateInfo); + $this->couponUsagePublisher->publish($updateInfo); } } diff --git a/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php b/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php new file mode 100644 index 0000000000000..0520cb658e408 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model; + +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; +use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Exception\NotFoundException; +use Psr\Log\LoggerInterface; + +/** + * Consumer for coupon usage update + */ +class CouponUsageConsumer +{ + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var CouponUsageProcessor + */ + private $processor; + + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @var UpdateInfoFactory + */ + private $updateInfoFactory; + + /** + * @param UpdateInfoFactory $updateInfoFactory + * @param CouponUsageProcessor $processor + * @param LoggerInterface $logger + * @param SerializerInterface $serializer + * @param EntityManager $entityManager + */ + public function __construct( + UpdateInfoFactory $updateInfoFactory, + CouponUsageProcessor $processor, + LoggerInterface $logger, + SerializerInterface $serializer, + EntityManager $entityManager + ) { + $this->updateInfoFactory = $updateInfoFactory; + $this->processor = $processor; + $this->logger = $logger; + $this->serializer = $serializer; + $this->entityManager = $entityManager; + } + + /** + * Process coupon usage update + * + * @param OperationInterface $operation + * @return void + * @throws \Exception + */ + public function process(OperationInterface $operation): void + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $updateInfo = $this->updateInfoFactory->create(); + $updateInfo->setData($data); + $this->processor->process($updateInfo); + } catch (NotFoundException $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __('Sorry, something went wrong during rule usage update. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } +} diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule.php b/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule.php index fb3f420a325e6..907d2c0494364 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule.php @@ -90,8 +90,9 @@ public function aggregate($from = null, $to = null) */ public function getUniqRulesNamesList() { - $connection = $this->getConnection(); - $tableName = $this->getTable('salesrule_coupon_aggregated'); + $resourceModel = $this->_createdatFactory->create(); + $connection = $resourceModel->getConnection(); + $tableName = $resourceModel->getMainTable(); $select = $connection->select()->from( $tableName, new \Zend_Db_Expr('DISTINCT rule_name') diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php index 1569c9551aa46..0adeedc32f759 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -111,7 +111,7 @@ public function calculate($rule, $item, $qty) $address, $baseRuleTotals ) : $baseRuleTotals; - $availableDiscountAmount = $this->cartFixedDiscountHelper + $maximumItemDiscount = $this->cartFixedDiscountHelper ->getDiscountAmount( $ruleDiscount, $qty, @@ -119,8 +119,8 @@ public function calculate($rule, $item, $qty) $baseRuleTotals, $discountType ); - $quoteAmount = $this->priceCurrency->convert($availableDiscountAmount, $store); - $baseDiscountAmount = min($baseItemPrice * $qty, $availableDiscountAmount); + $quoteAmount = $this->priceCurrency->convert($maximumItemDiscount, $store); + $baseDiscountAmount = min($baseItemPrice * $qty, $maximumItemDiscount); $this->deltaPriceRound->reset($discountType); } else { $baseRuleTotals = $shippingMethod ? diff --git a/app/code/Magento/SalesRule/Model/Service/CouponUsagePublisher.php b/app/code/Magento/SalesRule/Model/Service/CouponUsagePublisher.php new file mode 100644 index 0000000000000..1d1bbb1f63ed3 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Service/CouponUsagePublisher.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Service; + +use Magento\Framework\Bulk\BulkManagementInterface; +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\Framework\DataObject\IdentityGeneratorInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\Bulk\OperationInterface; +use Magento\Authorization\Model\UserContextInterface; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; + +/** + * Scheduler for coupon usage queue + */ +class CouponUsagePublisher +{ + private const TOPIC_NAME = 'sales.rule.update.coupon.usage'; + + /** + * @var BulkManagementInterface + */ + private $bulkManagement; + + /** + * @var OperationInterfaceFactory + */ + private $operationFactory; + + /** + * @var IdentityGeneratorInterface + */ + private $identityService; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @param BulkManagementInterface $bulkManagement + * @param OperationInterfaceFactory $operartionFactory + * @param IdentityGeneratorInterface $identityService + * @param SerializerInterface $serializer + * @param UserContextInterface $userContext + */ + public function __construct( + BulkManagementInterface $bulkManagement, + OperationInterfaceFactory $operartionFactory, + IdentityGeneratorInterface $identityService, + SerializerInterface $serializer, + UserContextInterface $userContext + ) { + $this->bulkManagement = $bulkManagement; + $this->operationFactory = $operartionFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; + $this->userContext = $userContext; + } + + /** + * Publish sales rule usage info into the queue + * + * @param string $updateInfo + * @return boolean + */ + public function publish(UpdateInfo $updateInfo): bool + { + $bulkUuid = $this->identityService->generateId(); + $bulkDescription = __('Rule processing: %1', implode(',', $updateInfo->getAppliedRuleIds())); + + $data = [ + 'data' => [ + 'bulk_uuid' => $bulkUuid, + 'topic_name' => self::TOPIC_NAME, + 'serialized_data' => $this->serializer->serialize($updateInfo->getData()), + 'status' => OperationInterface::STATUS_TYPE_OPEN, + ] + ]; + $operation = $this->operationFactory->create($data); + + return $this->bulkManagement->scheduleBulk( + $bulkUuid, + [$operation], + $bulkDescription, + $this->userContext->getUserId() + ); + } +} diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index cc0333480f7b0..7b3a6b15b7a32 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -318,7 +318,7 @@ public function process(AbstractItem $item) public function processShippingAmount(Address $address) { $shippingAmount = $address->getShippingAmountForDiscount(); - if ($shippingAmount !== null) { + if (!empty($shippingAmount)) { $baseShippingAmount = $address->getBaseShippingAmountForDiscount(); } else { $shippingAmount = $address->getShippingAmount(); diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 4a39e1237841d..4c1558baa767d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -261,6 +261,9 @@ <entity name="SalesRuleNoCouponWithFixedDiscount" extends="ApiCartRule"> <data key="simple_action">by_fixed</data> </entity> + <entity name="SalesRuleNoCouponWithFixedDiscountWholeCart" extends="ApiCartRule"> + <data key="simple_action">cart_fixed</data> + </entity> <entity name="ActiveSalesRuleWithPercentPriceDiscountCoupon"> <data key="name" unique="suffix">Cart Price Rule with Specific Coupon</data> <data key="description">Description for Cart Price Rule</data> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleQueueData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleQueueData.xml new file mode 100644 index 0000000000000..5ee98b49bd007 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleQueueData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesRuleConsumerData"> + <data key="consumerName">sales.rule.update.coupon.usage</data> + <data key="messageLimit">10</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAssertFixedCartDiscountAmountForBundleProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAssertFixedCartDiscountAmountForBundleProductTest.xml new file mode 100644 index 0000000000000..42e6d9e1d5b09 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAssertFixedCartDiscountAmountForBundleProductTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAssertFixedCartDiscountAmountForBundleProductTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Fixed Amount Cart Price Rule"/> + <title value="Checking Fixed Amount Cart Price Rule is correctly applied to bundle products"/> + <description value="Checking Fixed Amount Cart Price Rule is correctly applied to bundle products"/> + <severity value="AVERAGE"/> + <group value="SalesRule"/> + <testCaseId value="MC-39480"/> + </annotations> + <before> + <createData entity="SalesRuleNoCouponWithFixedDiscountWholeCart" stepKey="createSalesRule"/> + <actionGroup ref="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup" stepKey="createBundleProduct"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCatalogInventory"> + <argument name="indices" value="cataloginventory_stock"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + <deleteData createDataKey="createBundleProductCreateBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1CreateBundleProduct" stepKey="deleteSimpleProduct1"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createBundleProductCreateBundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="selectOption0Product1"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="checkOption0Product1"/> + <checkOption selector="{{StorefrontBundledSection.radioButtonOptionTwoProducts('Radio Buttons Option', '1')}}" stepKey="selectOption1Product0"/> + <seeCheckboxIsChecked selector="{{StorefrontBundledSection.radioButtonOptionTwoProducts('Radio Buttons Option', '1')}}" stepKey="checkOption1Product0"/> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts('Checkbox Option', '1')}}" stepKey="selectOption2Product0"/> + <seeCheckboxIsChecked selector="{{StorefrontBundledSection.checkboxOptionThreeProducts('Checkbox Option', '1')}}" stepKey="checkOption2Product0"/> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts('Checkbox Option', '2')}}" stepKey="selectOption2Product1"/> + <seeCheckboxIsChecked selector="{{StorefrontBundledSection.checkboxOptionThreeProducts('Checkbox Option', '1')}}" stepKey="checkOption2Product1"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssert" after="openShoppingCartPage"> + <argument name="subtotal" value="60.00"/> + <argument name="shipping" value="5.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="15.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index 159f2a1a82ece..18de326a2919d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -102,6 +102,12 @@ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> + <!-- Start the usage processing consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startUsageProcessingMessageQueue1"> + <argument name="consumerName" value="{{SalesRuleConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{SalesRuleConsumerData.messageLimit}}"/> + </actionGroup> + <!-- Step: 9-10. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage1"/> <waitForPageLoad stepKey="waitForPageLoad3"/> @@ -142,6 +148,12 @@ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage1"/> + <!-- Start the usage processing consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startUsageProcessingMessageQueue2"> + <argument name="consumerName" value="{{SalesRuleConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{SalesRuleConsumerData.messageLimit}}"/> + </actionGroup> + <!-- Step; 15-16. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage3"/> <waitForPageLoad stepKey="waitForPageLoad5"/> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/RuleTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/RuleTest.php index 1a302747fe454..9cb9767f47932 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/RuleTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/RuleTest.php @@ -7,13 +7,13 @@ namespace Magento\SalesRule\Test\Unit\Model\ResourceModel\Report; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\DB\Select; use Magento\Framework\DB\Select\SelectRenderer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Reports\Model\FlagFactory; use Magento\SalesRule\Model\ResourceModel\Report\Rule; +use Magento\SalesRule\Model\ResourceModel\Report\Rule\Createdat; use Magento\SalesRule\Model\ResourceModel\Report\Rule\CreatedatFactory; use Magento\SalesRule\Model\ResourceModel\Report\Rule\UpdatedatFactory; use PHPUnit\Framework\TestCase; @@ -84,14 +84,20 @@ function ($value) { [$this, 'fetchAllCallback'] ); - $resourceMock = $this->createMock(ResourceConnection::class); - $resourceMock->expects($this->any())->method('getConnection')->willReturn($connectionMock); - $resourceMock->expects($this->once())->method('getTableName')->willReturn(self::TABLE_NAME); - $flagFactory = $this->createMock(FlagFactory::class); - $createdatFactoryMock = $this->createPartialMock( + + $createdatResourceModel = $this->createConfiguredMock( + Createdat::class, + [ + 'getConnection' => $connectionMock, + 'getMainTable' => self::TABLE_NAME, + ] + ); + $createdatFactoryMock = $this->createConfiguredMock( CreatedatFactory::class, - ['create'] + [ + 'create' => $createdatResourceModel + ] ); $updatedatFactoryMock = $this->createPartialMock( UpdatedatFactory::class, @@ -102,7 +108,6 @@ function ($value) { $model = $objectHelper->getObject( Rule::class, [ - 'resource' => $resourceMock, 'reportsFlagFactory' => $flagFactory, 'createdatFactory' => $createdatFactoryMock, 'updatedatFactory' => $updatedatFactoryMock diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 4224cfafb3c8c..c895802153f45 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -33,6 +33,7 @@ use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Zend_Db_Select_Exception; /** * Test sales rule model validator @@ -538,7 +539,7 @@ public function testProcessShippingAmountProcessDisabled() * @param int $ruleDiscount * @param int $shippingDiscount * @dataProvider dataProviderActions - * @throws \Zend_Db_Select_Exception + * @throws Zend_Db_Select_Exception */ public function testProcessShippingAmountActions($action, $ruleDiscount, $shippingDiscount): void { @@ -595,6 +596,86 @@ public static function dataProviderActions() ]; } + /** + * Tests shipping amount with full discount action. + * + * @dataProvider dataProviderForFullShippingDiscount + * @param string $action + * @param float $ruleDiscount + * @param float $shippingDiscount + * @param float $shippingAmount + * @param float $quoteBaseSubTotal + * @throws Zend_Db_Select_Exception + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testProcessShippingAmountWithFullFixedPercentDiscount( + string $action, + float $ruleDiscount, + float $shippingDiscount, + float $shippingAmount, + float $quoteBaseSubTotal + ): void { + $ruleMock = $this->getMockBuilder(Rule::class) + ->disableOriginalConstructor() + ->setMethods(['getApplyToShipping', 'getSimpleAction', 'getDiscountAmount']) + ->getMock(); + $ruleMock->method('getApplyToShipping') + ->willReturn(true); + $ruleMock->method('getDiscountAmount') + ->willReturn($ruleDiscount); + $ruleMock->method('getSimpleAction') + ->willReturn($action); + + $iterator = new \ArrayIterator([$ruleMock]); + $this->ruleCollection->method('getIterator') + ->willReturn($iterator); + + $this->utility->method('canProcessRule') + ->willReturn(true); + + $this->priceCurrency->method('convert') + ->willReturn($ruleDiscount); + + $this->priceCurrency->method('roundPrice') + ->willReturn(round($shippingDiscount, 2)); + + $this->model->init( + $this->model->getWebsiteId(), + $this->model->getCustomerGroupId(), + $this->model->getCouponCode() + ); + + $addressMock = $this->setupAddressMock($shippingAmount, $quoteBaseSubTotal); + + self::assertInstanceOf(Validator::class, $this->model->processShippingAmount($addressMock)); + self::assertEquals($shippingDiscount, $addressMock->getShippingDiscountAmount()); + } + + /** + * Get data provider array for full shipping discount action + * + * @return array + */ + public function dataProviderForFullShippingDiscount(): array + { + return [ + 'verify shipping discount when shipping amount is greater than zero' => [ + Rule::BY_PERCENT_ACTION, + 100.00, + 5.0, + 5.0, + 10.0 + ], + 'verify shipping discount when shipping amount is zero' => [ + Rule::BY_PERCENT_ACTION, + 100.00, + 5.0, + 0, + 10.0 + ] + ]; + } + /** * @param float $shippingAmount * @param float $quoteBaseSubTotal diff --git a/app/code/Magento/SalesRule/composer.json b/app/code/Magento/SalesRule/composer.json index 572e191093275..20246f67e337e 100644 --- a/app/code/Magento/SalesRule/composer.json +++ b/app/code/Magento/SalesRule/composer.json @@ -7,6 +7,7 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/framework-bulk": "*", "magento/module-backend": "*", "magento/module-catalog": "*", "magento/module-catalog-rule": "*", @@ -25,7 +26,8 @@ "magento/module-widget": "*", "magento/module-captcha": "*", "magento/module-checkout": "*", - "magento/module-authorization": "*" + "magento/module-authorization": "*", + "magento/module-asynchronous-operations": "*" }, "suggest": { "magento/module-sales-rule-sample-data": "*" diff --git a/app/code/Magento/SalesRule/etc/communication.xml b/app/code/Magento/SalesRule/etc/communication.xml index 4c905fa83e2fd..786e866f0e3c5 100644 --- a/app/code/Magento/SalesRule/etc/communication.xml +++ b/app/code/Magento/SalesRule/etc/communication.xml @@ -9,4 +9,7 @@ <topic name="sales_rule.codegenerator" request="Magento\SalesRule\Api\Data\CouponGenerationSpecInterface"> <handler name="codegeneratorProcessor" type="Magento\SalesRule\Model\Coupon\Consumer" method="process" /> </topic> + <topic name="sales.rule.update.coupon.usage" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="sales.rule.update.coupon.usage" type="Magento\SalesRule\Model\CouponUsageConsumer" method="process" /> + </topic> </config> diff --git a/app/code/Magento/SalesRule/etc/queue.xml b/app/code/Magento/SalesRule/etc/queue.xml index 8217a0b9f6c1a..87dce71b53054 100644 --- a/app/code/Magento/SalesRule/etc/queue.xml +++ b/app/code/Magento/SalesRule/etc/queue.xml @@ -9,4 +9,7 @@ <broker topic="sales_rule.codegenerator" exchange="magento-db" type="db"> <queue name="codegenerator" consumer="codegeneratorProcessor" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\SalesRule\Model\Coupon\Consumer::process"/> </broker> + <broker topic="sales.rule.update.coupon.usage" exchange="magento-db" type="db"> + <queue name="sales.rule.update.coupon.usage" consumer="sales.rule.update.coupon.usage" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\SalesRule\Model\CouponUsageConsumer::process"/> + </broker> </config> diff --git a/app/code/Magento/SalesRule/etc/queue_consumer.xml b/app/code/Magento/SalesRule/etc/queue_consumer.xml index 9eb585f48e8e3..bcebaf6a543b9 100644 --- a/app/code/Magento/SalesRule/etc/queue_consumer.xml +++ b/app/code/Magento/SalesRule/etc/queue_consumer.xml @@ -7,4 +7,5 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> <consumer name="codegeneratorProcessor" queue="codegenerator" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\SalesRule\Model\Coupon\Consumer::process" /> + <consumer name="sales.rule.update.coupon.usage" queue="sales.rule.update.coupon.usage" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\SalesRule\Model\CouponUsageConsumer::process" /> </config> diff --git a/app/code/Magento/SalesRule/etc/queue_publisher.xml b/app/code/Magento/SalesRule/etc/queue_publisher.xml index 0863fba2307c5..f1b8bddf2c090 100644 --- a/app/code/Magento/SalesRule/etc/queue_publisher.xml +++ b/app/code/Magento/SalesRule/etc/queue_publisher.xml @@ -9,4 +9,7 @@ <publisher topic="sales_rule.codegenerator"> <connection name="db" exchange="magento-db" /> </publisher> + <publisher topic="sales.rule.update.coupon.usage"> + <connection name="db" exchange="magento-db" /> + </publisher> </config> diff --git a/app/code/Magento/SalesRule/etc/queue_topology.xml b/app/code/Magento/SalesRule/etc/queue_topology.xml index fd6a9bf36721c..3902c8a3ab36f 100644 --- a/app/code/Magento/SalesRule/etc/queue_topology.xml +++ b/app/code/Magento/SalesRule/etc/queue_topology.xml @@ -8,5 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> <exchange name="magento-db" type="topic" connection="db"> <binding id="codegeneratorBinding" topic="sales_rule.codegenerator" destinationType="queue" destination="codegenerator"/> + <binding id="couponUsageBinding" topic="sales.rule.update.coupon.usage" destinationType="queue" destination="sales.rule.update.coupon.usage"/> </exchange> </config> diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml index 34379fd6d2e4a..088bd5eed4cc3 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml @@ -14,5 +14,6 @@ <element name="searchMiniForm" type="input" selector="#search_mini_form"/> <element name="searchDropDownSuggestion" type="text" selector="//div[@id='search_autocomplete']/ul/li/span"/> <element name="searchDropDownName" type="text" selector="//div[@id='search_autocomplete']//span[contains(., '{{searchQuery}}')]" parameterized="true"/> + <element name="searchMagnifierIcon" type="text" selector="//*[@id='search_mini_form']//label[@data-role='minisearch-label']"/> </section> </sections> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchFieldVisibilityWhenSuggestionDisabledTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchFieldVisibilityWhenSuggestionDisabledTest.xml new file mode 100644 index 0000000000000..cdd52bd2b524e --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchFieldVisibilityWhenSuggestionDisabledTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySearchFieldVisibilityWhenSuggestionDisabledTest"> + <annotations> + <stories value="Search Term"/> + <title value="Mini search field appears if suggestions was disabled"/> + <description value="Mini search field appears if suggestions was disabled"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-39443"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Disable search suggestion --> + <magentoCLI command="config:set catalog/search/search_suggestion_enabled 0" stepKey="disableSearchSuggestion"/> + + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheFirst"> + <argument name="tags" value="config full_page"/> + </actionGroup> + </before> + + <after> + <!-- Enable search suggestion back --> + <magentoCLI command="config:set catalog/search/search_suggestion_enabled 1" stepKey="disableSearchSuggestion"/> + + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheSecond"> + <argument name="tags" value="config full_page"/> + </actionGroup> + + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Go to storefront home page --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> + + <resizeWindow width="767" height="720" stepKey="resizeWindowToMobileView"/> + + <click selector="{{StorefrontQuickSearchSection.searchMagnifierIcon}}" stepKey="clickOnMagnifierSearchIcon"/> + + <waitForElementVisible selector="{{StorefrontQuickSearchSection.searchPhrase}}" after="clickOnMagnifierSearchIcon" stepKey="seeInputSearchActive"/> + + </test> +</tests> diff --git a/app/code/Magento/Search/ViewModel/ConfigProvider.php b/app/code/Magento/Search/ViewModel/ConfigProvider.php index be3366e62e965..b1db5b57e13e0 100644 --- a/app/code/Magento/Search/ViewModel/ConfigProvider.php +++ b/app/code/Magento/Search/ViewModel/ConfigProvider.php @@ -10,6 +10,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\View\Element\Block\ArgumentInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Search\Helper\Data as SearchHelper; /** * View model for search @@ -26,13 +27,31 @@ class ConfigProvider implements ArgumentInterface */ private $scopeConfig; + /** + * @var SearchHelper + */ + private $searchHelper; + /** * @param ScopeConfigInterface $scopeConfig + * @param SearchHelper $searchHelper */ public function __construct( - ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + SearchHelper $searchHelper ) { $this->scopeConfig = $scopeConfig; + $this->searchHelper = $searchHelper; + } + + /** + * Retrieve search helper instance for template view + * + * @return SearchHelper + */ + public function getSearchHelperData(): SearchHelper + { + return $this->searchHelper; } /** diff --git a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml index 80e720e2c2fe2..c6b2a6a729575 100644 --- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml +++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml @@ -4,40 +4,42 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Framework\View\Element\Template */ -/** @var $helper \Magento\Search\Helper\Data */ +/** @var $escaper \Magento\Framework\Escaper */ /** @var $configProvider \Magento\Search\ViewModel\ConfigProvider */ -$helper = $this->helper(\Magento\Search\Helper\Data::class); $configProvider = $block->getData('configProvider'); +/** @var $helper \Magento\Search\Helper\Data */ +$helper = $configProvider->getSearchHelperData(); +$allowedSuggestion = $configProvider->isSuggestionsAllowed(); +$quickSearchUrl = $allowedSuggestion ? $escaper->escapeUrl($helper->getSuggestUrl()) : ''; ?> <div class="block block-search"> - <div class="block block-title"><strong><?= $block->escapeHtml(__('Search')) ?></strong></div> + <div class="block block-title"><strong><?= $escaper->escapeHtml(__('Search')) ?></strong></div> <div class="block block-content"> <form class="form minisearch" id="search_mini_form" - action="<?= $block->escapeUrl($helper->getResultUrl()) ?>" method="get"> + action="<?= $escaper->escapeUrl($helper->getResultUrl()) ?>" method="get"> <div class="field search"> <label class="label" for="search" data-role="minisearch-label"> - <span><?= $block->escapeHtml(__('Search')) ?></span> + <span><?= $escaper->escapeHtml(__('Search')) ?></span> </label> <div class="control"> <input id="search" - <?php if ($configProvider->isSuggestionsAllowed()):?> - data-mage-init='{"quickSearch":{ - "formSelector":"#search_mini_form", - "url":"<?= $block->escapeUrl($helper->getSuggestUrl())?>", - "destinationSelector":"#search_autocomplete", - "minSearchLength":"<?= $block->escapeHtml($helper->getMinQueryLength()) ?>"} - }' - <?php endif;?> + data-mage-init='{ + "quickSearch": { + "formSelector": "#search_mini_form", + "url": "<?= /* @noEscape */ $quickSearchUrl ?>", + "destinationSelector": "#search_autocomplete", + "minSearchLength": "<?= $escaper->escapeHtml($helper->getMinQueryLength()) ?>" + } + }' type="text" - name="<?= $block->escapeHtmlAttr($helper->getQueryParamName()) ?>" + name="<?= $escaper->escapeHtmlAttr($helper->getQueryParamName()) ?>" value="<?= /* @noEscape */ $helper->getEscapedQueryText() ?>" - placeholder="<?= $block->escapeHtmlAttr(__('Search entire store here...')) ?>" + placeholder="<?= $escaper->escapeHtmlAttr(__('Search entire store here...')) ?>" class="input-text" - maxlength="<?= $block->escapeHtmlAttr($helper->getMaxQueryLength()) ?>" + maxlength="<?= $escaper->escapeHtmlAttr($helper->getMaxQueryLength()) ?>" role="combobox" aria-haspopup="false" aria-autocomplete="both" @@ -49,11 +51,11 @@ $configProvider = $block->getData('configProvider'); </div> <div class="actions"> <button type="submit" - title="<?= $block->escapeHtml(__('Search')) ?>" - class="action search" - aria-label="Search" + title="<?= $escaper->escapeHtml(__('Search')) ?>" + class="action search" + aria-label="Search" > - <span><?= $block->escapeHtml(__('Search')) ?></span> + <span><?= $escaper->escapeHtml(__('Search')) ?></span> </button> </div> </form> diff --git a/app/code/Magento/Search/view/frontend/web/js/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js index 9b4c814f73d73..b8034fead76d0 100644 --- a/app/code/Magento/Search/view/frontend/web/js/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/js/form-mini.js @@ -35,12 +35,12 @@ define([ selectClass: 'selected', template: '<li class="<%- data.row_class %>" id="qs-option-<%- data.index %>" role="option">' + - '<span class="qs-option-name">' + - ' <%- data.title %>' + - '</span>' + - '<span aria-hidden="true" class="amount">' + - '<%- data.num_results %>' + - '</span>' + + '<span class="qs-option-name">' + + ' <%- data.title %>' + + '</span>' + + '<span aria-hidden="true" class="amount">' + + '<%- data.num_results %>' + + '</span>' + '</li>', submitBtn: 'button[type="submit"]', searchLabel: '[data-role=minisearch-label]', @@ -300,60 +300,63 @@ define([ if (value.length >= parseInt(this.options.minSearchLength, 10)) { this.submitBtn.disabled = false; - $.getJSON(this.options.url, { - q: value - }, $.proxy(function (data) { - if (data.length) { - $.each(data, function (index, element) { - var html; - - element.index = index; - html = template({ - data: element - }); - dropdown.append(html); - }); - - this._resetResponseList(true); - this.responseList.indexList = this.autoComplete.html(dropdown) - .css(clonePosition) - .show() - .find(this.options.responseFieldElements + ':visible'); - - this.element.removeAttr('aria-activedescendant'); + if (this.options.url !== '') { //eslint-disable-line eqeqeq + $.getJSON(this.options.url, { + q: value + }, $.proxy(function (data) { + if (data.length) { + $.each(data, function (index, element) { + var html; + + element.index = index; + html = template({ + data: element + }); + dropdown.append(html); + }); - if (this.responseList.indexList.length) { - this._updateAriaHasPopup(true); + this._resetResponseList(true); + + this.responseList.indexList = this.autoComplete.html(dropdown) + .css(clonePosition) + .show() + .find(this.options.responseFieldElements + ':visible'); + + this.element.removeAttr('aria-activedescendant'); + + if (this.responseList.indexList.length) { + this._updateAriaHasPopup(true); + } else { + this._updateAriaHasPopup(false); + } + + this.responseList.indexList + .on('click', function (e) { + this.responseList.selected = $(e.currentTarget); + this.searchForm.trigger('submit'); + }.bind(this)) + .on('mouseenter mouseleave', function (e) { + this.responseList.indexList.removeClass(this.options.selectClass); + $(e.target).addClass(this.options.selectClass); + this.responseList.selected = $(e.target); + this.element.attr('aria-activedescendant', $(e.target).attr('id')); + }.bind(this)) + .on('mouseout', function (e) { + if (!this._getLastElement() && + this._getLastElement().hasClass(this.options.selectClass)) { + $(e.target).removeClass(this.options.selectClass); + this._resetResponseList(false); + } + }.bind(this)); } else { + this._resetResponseList(true); + this.autoComplete.hide(); this._updateAriaHasPopup(false); + this.element.removeAttr('aria-activedescendant'); } - - this.responseList.indexList - .on('click', function (e) { - this.responseList.selected = $(e.currentTarget); - this.searchForm.trigger('submit'); - }.bind(this)) - .on('mouseenter mouseleave', function (e) { - this.responseList.indexList.removeClass(this.options.selectClass); - $(e.target).addClass(this.options.selectClass); - this.responseList.selected = $(e.target); - this.element.attr('aria-activedescendant', $(e.target).attr('id')); - }.bind(this)) - .on('mouseout', function (e) { - if (!this._getLastElement() && - this._getLastElement().hasClass(this.options.selectClass)) { - $(e.target).removeClass(this.options.selectClass); - this._resetResponseList(false); - } - }.bind(this)); - } else { - this._resetResponseList(true); - this.autoComplete.hide(); - this._updateAriaHasPopup(false); - this.element.removeAttr('aria-activedescendant'); - } - }, this)); + }, this)); + } } else { this._resetResponseList(true); this.autoComplete.hide(); diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php index 100ba029beabd..0c9738540322c 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php @@ -8,10 +8,11 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; +use Magento\Sales\Helper\Data as SalesData; use Magento\Sales\Model\Order\Shipment\Validation\QuantityValidator; /** - * Class Save + * Controller for generation of new Shipments from Backend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface @@ -43,19 +44,26 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac */ private $shipmentValidator; + /** + * @var SalesData + */ + private $salesData; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader * @param \Magento\Shipping\Model\Shipping\LabelGenerator $labelGenerator * @param \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender * @param \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface|null $shipmentValidator + * @param SalesData $salesData */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader, \Magento\Shipping\Model\Shipping\LabelGenerator $labelGenerator, \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender, - \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface $shipmentValidator = null + \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface $shipmentValidator = null, + SalesData $salesData = null ) { parent::__construct($context); @@ -64,6 +72,8 @@ public function __construct( $this->shipmentSender = $shipmentSender; $this->shipmentValidator = $shipmentValidator ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface::class); + $this->salesData = $salesData ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(SalesData::class); } /** @@ -109,6 +119,7 @@ public function execute() } $data = $this->getRequest()->getParam('shipment'); + $orderId = $this->getRequest()->getParam('order_id'); if (!empty($data['comment_text'])) { $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setCommentText($data['comment_text']); @@ -118,7 +129,7 @@ public function execute() $responseAjax = new \Magento\Framework\DataObject(); try { - $this->shipmentLoader->setOrderId($this->getRequest()->getParam('order_id')); + $this->shipmentLoader->setOrderId($orderId); $this->shipmentLoader->setShipmentId($this->getRequest()->getParam('shipment_id')); $this->shipmentLoader->setShipment($data); $this->shipmentLoader->setTracking($this->getRequest()->getParam('tracking')); @@ -143,7 +154,7 @@ public function execute() $this->messageManager->addErrorMessage( __("Shipment Document Validation Error(s):\n" . implode("\n", $validationResult->getMessages())) ); - return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + return $resultRedirect->setPath('*/*/new', ['order_id' => $orderId]); } $shipment->register(); @@ -156,7 +167,7 @@ public function execute() $this->_saveShipment($shipment); - if (!empty($data['send_email'])) { + if (!empty($data['send_email']) && $this->salesData->canSendNewShipmentEmail()) { $this->shipmentSender->send($shipment); } @@ -173,7 +184,7 @@ public function execute() $responseAjax->setMessage($e->getMessage()); } else { $this->messageManager->addErrorMessage($e->getMessage()); - return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + return $resultRedirect->setPath('*/*/new', ['order_id' => $orderId]); } } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -182,13 +193,13 @@ public function execute() $responseAjax->setMessage(__('An error occurred while creating shipping label.')); } else { $this->messageManager->addErrorMessage(__('Cannot save shipment.')); - return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + return $resultRedirect->setPath('*/*/new', ['order_id' => $orderId]); } } if ($isNeedCreateLabel) { return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setJsonData($responseAjax->toJson()); } - return $resultRedirect->setPath('sales/order/view', ['order_id' => $shipment->getOrderId()]); + return $resultRedirect->setPath('sales/order/view', ['order_id' => $orderId]); } } diff --git a/app/code/Magento/Shipping/Model/Config.php b/app/code/Magento/Shipping/Model/Config.php index 26a76c90d3c85..565901ebe8ef0 100644 --- a/app/code/Magento/Shipping/Model/Config.php +++ b/app/code/Magento/Shipping/Model/Config.php @@ -4,16 +4,21 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Shipping\Model; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; use Magento\Shipping\Model\Carrier\AbstractCarrierInterface; +use Magento\Store\Model\ScopeInterface; /** - * Class Config + * Config model for shipping * @api * @since 100.0.2 */ -class Config extends \Magento\Framework\DataObject +class Config extends DataObject { /** * Shipping origin settings @@ -29,25 +34,25 @@ class Config extends \Magento\Framework\DataObject /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $_scopeConfig; /** - * @var \Magento\Shipping\Model\CarrierFactory + * @var CarrierFactory */ protected $_carrierFactory; /** * Constructor * - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory + * @param ScopeConfigInterface $scopeConfig + * @param CarrierFactory $carrierFactory * @param array $data */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Shipping\Model\CarrierFactory $carrierFactory, + ScopeConfigInterface $scopeConfig, + CarrierFactory $carrierFactory, array $data = [] ) { $this->_scopeConfig = $scopeConfig; @@ -58,17 +63,17 @@ public function __construct( /** * Retrieve active system carriers * - * @param mixed $store - * @return AbstractCarrierInterface[] + * @param mixed $store + * @return AbstractCarrierInterface[] */ public function getActiveCarriers($store = null) { $carriers = []; - $config = $this->_scopeConfig->getValue('carriers', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + $config = $this->getCarriersConfig($store); foreach (array_keys($config) as $carrierCode) { if ($this->_scopeConfig->isSetFlag( 'carriers/' . $carrierCode . '/active', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store )) { $carrierModel = $this->_carrierFactory->create($carrierCode, $store); @@ -77,25 +82,38 @@ public function getActiveCarriers($store = null) } } } + return $carriers; } /** * Retrieve all system carriers * - * @param mixed $store - * @return AbstractCarrierInterface[] + * @param mixed $store + * @return AbstractCarrierInterface[] */ public function getAllCarriers($store = null) { $carriers = []; - $config = $this->_scopeConfig->getValue('carriers', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + $config = $this->getCarriersConfig($store); foreach (array_keys($config) as $carrierCode) { $model = $this->_carrierFactory->create($carrierCode, $store); if ($model) { $carriers[$carrierCode] = $model; } } + return $carriers; } + + /** + * Returns carriers config by store + * + * @param mixed $store + * @return array + */ + private function getCarriersConfig($store = null): array + { + return $this->_scopeConfig->getValue('carriers', ScopeInterface::SCOPE_STORE, $store) ?: []; + } } diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml index d448f51a00406..c174517375779 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml @@ -27,6 +27,8 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php index 1d71f8a0d8e0c..afd5f8819a8e0 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -21,6 +21,7 @@ use Magento\Framework\Message\Manager; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Helper\Data as SalesData; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\ShipmentSender; use Magento\Sales\Model\Order\Shipment; @@ -119,6 +120,11 @@ class SaveTest extends TestCase */ private $validationResult; + /** + * @var SalesData|MockObject + */ + private $salesData; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -140,7 +146,14 @@ protected function setUp(): void ->getMock(); $this->shipmentSender = $this->getMockBuilder(ShipmentSender::class) ->disableOriginalConstructor() - ->setMethods([]) + ->setMethods(['send']) + ->getMock(); + $this->shipmentSender->expects($this->any()) + ->method('send') + ->willReturn(true); + $this->salesData = $this->getMockBuilder(SalesData::class) + ->disableOriginalConstructor() + ->setMethods(['canSendNewShipmentEmail']) ->getMock(); $this->objectManager = $this->getMockForAbstractClass(ObjectManagerInterface::class); $this->context = $this->createPartialMock(Context::class, [ @@ -232,7 +245,8 @@ protected function setUp(): void 'shipmentLoader' => $this->shipmentLoader, 'request' => $this->request, 'response' => $this->response, - 'shipmentValidator' => $this->shipmentValidatorMock + 'shipmentValidator' => $this->shipmentValidatorMock, + 'salesData' => $this->salesData ] ); } @@ -240,11 +254,19 @@ protected function setUp(): void /** * @param bool $formKeyIsValid * @param bool $isPost + * @param string $sendEmail + * @param bool $emailEnabled + * @param bool $shouldEmailBeSent * @dataProvider executeDataProvider * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testExecute($formKeyIsValid, $isPost) - { + public function testExecute( + $formKeyIsValid, + $isPost, + $sendEmail, + $emailEnabled, + $shouldEmailBeSent + ) { $this->formKeyValidator->expects($this->any()) ->method('validate') ->willReturn($formKeyIsValid); @@ -269,7 +291,7 @@ public function testExecute($formKeyIsValid, $isPost) $shipmentId = 1000012; $orderId = 10003; $tracking = []; - $shipmentData = ['items' => [], 'send_email' => '']; + $shipmentData = ['items' => [], 'send_email' => $sendEmail]; $shipment = $this->createPartialMock( Shipment::class, ['load', 'save', 'register', 'getOrder', 'getOrderId', '__wakeup'] @@ -287,6 +309,13 @@ public function testExecute($formKeyIsValid, $isPost) ] ); + $this->salesData->expects($this->any()) + ->method('canSendNewShipmentEmail') + ->willReturn($emailEnabled); + if ($shouldEmailBeSent) { + $this->shipmentSender->expects($this->once()) + ->method('send'); + } $this->shipmentLoader->expects($this->any()) ->method('setShipmentId') ->with($shipmentId); @@ -309,7 +338,7 @@ public function testExecute($formKeyIsValid, $isPost) ->willReturn($order); $order->expects($this->once()) ->method('setCustomerNoteNotify') - ->with(false); + ->with(!empty($sendEmail)); $this->labelGenerator->expects($this->any()) ->method('create') ->with($shipment, $this->request) @@ -340,7 +369,7 @@ public function testExecute($formKeyIsValid, $isPost) ->with(Session::class) ->willReturn($this->session); $arguments = ['order_id' => $orderId]; - $shipment->expects($this->once()) + $shipment->expects($this->any()) ->method('getOrderId') ->willReturn($orderId); $this->prepareRedirect($arguments); @@ -364,11 +393,22 @@ public function testExecute($formKeyIsValid, $isPost) */ public function executeDataProvider() { + /** + * bool $formKeyIsValid + * bool $isPost + * string $sendEmail + * bool $emailEnabled + * bool $shouldEmailBeSent + */ return [ - [false, false], - [true, false], - [false, true], - [true, true] + [false, false, '', false, false], + [true, false, '', false, false], + [false, true, '', false, false], + [true, true, '', false, false], + [true, true, '', true, false], + [true, true, 'on', false, false], + [true, true, 'on', true, true], + ]; } diff --git a/app/code/Magento/Shipping/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Shipping/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..e676b698c7f0e --- /dev/null +++ b/app/code/Magento/Shipping/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,152 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Shipping\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Shipping\Model\CarrierFactory; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Shipping\Model\Config; + +/** + * Test for \Magento\Shipping\Model\Config. + */ +class ConfigTest extends TestCase +{ + private const STUB_STORE_CODE = 'default'; + + /** + * @var array + */ + private $shippingCarriersData = [ + 'flatrate' => [ + 'active' => '1', + 'name' => 'Fixed', + 'title' => 'Flat Rate', + ], + 'tablerate' => [ + 'active' => '0', + 'name' => 'Table Rate', + 'title' => 'Best Way', + ] + ]; + + /** + * @var Config + */ + private $model; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var CarrierFactory|MockObject + */ + private $carrierFactoryMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->carrierFactoryMock = $this->createMock(CarrierFactory::class); + + $this->model = new Config($this->scopeConfigMock, $this->carrierFactoryMock, []); + } + + /** + * Get active carriers when there is no active on the store + * + * @return void + */ + public function testGetActiveCarriersWhenThereIsNoAvailable(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('carriers', ScopeInterface::SCOPE_STORE, null) + ->willReturn(null); + + $this->assertEquals([], $this->model->getActiveCarriers()); + } + + /** + * Test for getActiveCarriers + * + * @return void + */ + public function testGetActiveCarriers(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('carriers', ScopeInterface::SCOPE_STORE, self::STUB_STORE_CODE) + ->willReturn($this->shippingCarriersData); + + $this->scopeConfigMock->expects($this->exactly(2)) + ->method('isSetFlag') + ->withConsecutive( + ['carriers/flatrate/active', ScopeInterface::SCOPE_STORE, self::STUB_STORE_CODE], + ['carriers/tablerate/active', ScopeInterface::SCOPE_STORE, self::STUB_STORE_CODE], + ) + ->willReturnOnConsecutiveCalls( + true, + false, + ); + + $this->carrierFactoryMock->expects($this->once()) + ->method('create') + ->with('flatrate', self::STUB_STORE_CODE) + ->willReturn(true); + + $this->assertEquals(['flatrate' => true], $this->model->getActiveCarriers(self::STUB_STORE_CODE)); + } + + /** + * Get all carriers when there is no carriers available on the store + * + * @return void + */ + public function testGetAllCarriersWhenThereIsNoAvailable(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('carriers', ScopeInterface::SCOPE_STORE, null) + ->willReturn(null); + + $this->assertEquals([], $this->model->getAllCarriers()); + } + + /** + * Test for getAllCarriers + * + * @return void + */ + public function testGetAllCarriers(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('carriers', ScopeInterface::SCOPE_STORE, self::STUB_STORE_CODE) + ->willReturn($this->shippingCarriersData); + + $this->carrierFactoryMock->expects($this->exactly(2)) + ->method('create') + ->withConsecutive( + ['flatrate', self::STUB_STORE_CODE], + ['tablerate', self::STUB_STORE_CODE], + ) + ->willReturnOnConsecutiveCalls( + true, + false, + ); + + $this->assertEquals(['flatrate' => true], $this->model->getAllCarriers(self::STUB_STORE_CODE)); + } +} diff --git a/app/code/Magento/Store/Model/StoreResolver/GetStoresListByWebsiteIds.php b/app/code/Magento/Store/Model/StoreResolver/GetStoresListByWebsiteIds.php new file mode 100644 index 0000000000000..416537caaf0e0 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreResolver/GetStoresListByWebsiteIds.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreResolver; + +use Magento\Store\Api\StoreWebsiteRelationInterface; + +/** + * Retrieves store ids list array by website ids array + */ +class GetStoresListByWebsiteIds +{ + /** + * @var StoreWebsiteRelationInterface + */ + private $storeWebsiteRelation; + + /** + * @param StoreWebsiteRelationInterface $storeWebsiteRelation + */ + public function __construct(StoreWebsiteRelationInterface $storeWebsiteRelation) + { + $this->storeWebsiteRelation = $storeWebsiteRelation; + } + + /** + * Retrieve list of stores by website ids + * + * @param array $websiteIds + * @return array + */ + public function execute(array $websiteIds): array + { + $storeIdsArray = []; + foreach ($websiteIds as $websiteId) { + $storeIdsArray[] = $this->storeWebsiteRelation->getStoreByWebsiteId($websiteId); + } + + return array_merge([], ...$storeIdsArray); + } +} diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml index ab532538cc3f3..e05496efaa152 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml @@ -22,8 +22,10 @@ <createData entity="Simple_US_Customer" after="createCategory" stepKey="createCustomer"/> </before> <after> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createCustomer" before="goToProductAttributes" stepKey="deleteCustomer"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" after="deleteCustomer" stepKey="logoutFromCustomer"/> + <comment userInput="BIC workaround" stepKey="logoutFromCustomer"/> </after> <!-- Remove steps that are not used for this test --> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminGoToNewTaxRulePageActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminGoToNewTaxRulePageActionGroup.xml new file mode 100644 index 0000000000000..ce16cfb73df67 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminGoToNewTaxRulePageActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGoToNewTaxRulePageActionGroup"> + <annotations> + <description>Go to the create New Tax Rule page.</description> + </annotations> + <amOnPage url="{{AdminNewTaxRulePage.url}}" stepKey="goToNewTaxRulePage"/> + <waitForPageLoad stepKey="waitForTaxRulePage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminSaveTaxRuleActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminSaveTaxRuleActionGroup.xml new file mode 100644 index 0000000000000..cf9f734b06f2d --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminSaveTaxRuleActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSaveTaxRuleActionGroup"> + <annotations> + <description>Clicks the Save Rule button on the Tax Rule page.</description> + </annotations> + <waitForElementVisible selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="waitForSaveButton"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSave"/> + <waitForText selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rule." stepKey="waitForSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml index 84278468a0590..8bacc134c7ab8 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml @@ -154,8 +154,10 @@ </actionGroup> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress"> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertCurrentPageNumberActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertCurrentPageNumberActionGroup.xml new file mode 100644 index 0000000000000..1765dbe1e7fcd --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertCurrentPageNumberActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGridAssertCurrentPageNumberActionGroup"> + <annotations> + <description> + Assert current page number on admin grid + </description> + </annotations> + <arguments> + <argument name="expectedCurrentPageNumber" defaultValue="1" type="string"/> + </arguments> + + <seeInField selector="{{AdminDataGridPaginationSection.currentPage}}" userInput="{{expectedCurrentPageNumber}}" stepKey="seeCurrentPageNumberOnGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertTotalPageCountActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertTotalPageCountActionGroup.xml new file mode 100644 index 0000000000000..402bd077f9fbb --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertTotalPageCountActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGridAssertTotalPageCountActionGroup"> + <annotations> + <description> + Assert total page count on admin grid + </description> + </annotations> + <arguments> + <argument name="expectedTotalPageCount" defaultValue="1" type="string"/> + </arguments> + + <waitForElementVisible selector="{{AdminDataGridPaginationSection.totalPagesCount('expectedTotalPageCount')}}" stepKey="waitForTotalPagesCountToBeVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridGoToNextPageActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridGoToNextPageActionGroup.xml new file mode 100644 index 0000000000000..7da082a279688 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridGoToNextPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGridGoToNextPageActionGroup"> + <annotations> + <description> + Go to next page of the admin grid. + </description> + </annotations> + + <click selector="{{AdminDataGridPaginationSection.nextPage}}" stepKey="clickNextPageOnGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml index 51cebdb01a74d..eaea88fdddb03 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml @@ -20,6 +20,7 @@ <element name="previousPage" type="button" selector="div.admin__data-grid-pager > button.action-previous" timeout="30"/> <element name="currentPage" type="input" selector="div.admin__data-grid-pager > input[data-ui-id='current-page-input']"/> <element name="totalPages" type="text" selector="div.admin__data-grid-pager > label"/> + <element name="totalPagesCount" type="text" selector="//div[@class='admin__data-grid-pager']//label[@class='admin__control-support-text' and .='of {{arg1}}']" parameterized="true"/> <element name="perPageDropDownValue" type="input" selector=".selectmenu-value input" timeout="30"/> <element name="selectedPage" type="input" selector="#sales_order_create_search_grid_page-current" timeout="30"/> <element name="nextPageActive" type="button" selector="div.admin__data-grid-pager > button.action-next:not(.disabled)" timeout="30"/> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml index c5b000259e265..d2d39076bcfbb 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml @@ -22,5 +22,6 @@ <element name="rowTemplateStrict" type="block" selector="//tbody/tr[td[*[text()[normalize-space()='{{text}}']]]]" parameterized="true" /> <element name="rowTemplate" type="block" selector="//tbody/tr[td[*[contains(.,normalize-space('{{text}}'))]]]" parameterized="true" timeout="30" /> <element name="firstNotEmptyRow" type="block" selector="table.data-grid tbody tr[data-role=row]:not(.data-grid-tr-no-data):nth-of-type(1)" timeout="30"/> + <element name="firstNotEmptyRow2" type="block" selector="table.data-grid tbody tr:not(.data-grid-tr-no-data):nth-of-type(1)" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/website.js b/app/code/Magento/Ui/view/base/web/js/form/element/website.js index 3c99cc0874cf9..0d1ed2d9961a1 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/website.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/website.js @@ -26,10 +26,6 @@ define([ initialize: function () { this._super(); - if (this.customerId || this.isGlobalScope) { - this.disable(true); - } - return this; } }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js b/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js index 534d292809ed1..5f6c21cf6167f 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js @@ -36,7 +36,8 @@ define([ imports: { totalSelected: '${ $.selectProvider }:totalSelected', totalRecords: '${ $.provider }:data.totalRecords', - filters: '${ $.provider }:params.filters' + filters: '${ $.provider }:params.filters', + keywordUpdated: '${ $.provider }:params.keywordUpdated' }, exports: { @@ -58,7 +59,8 @@ define([ 'pages': 'onPagesChange', 'pageSize': 'onPageSizeChange', 'totalRecords': 'updateCounter', - '${ $.provider }:params.filters': 'goFirst' + '${ $.provider }:params.filters': 'goFirst', + '${ $.provider }:params.search': 'onSearchUpdate' }, modules: { @@ -282,6 +284,17 @@ define([ */ onPagesChange: function () { this.updateCursor(); + }, + + /** + * Resent the pagination to Page 1 on search keyword update + */ + onSearchUpdate: function () { + if (!_.isUndefined(this.keywordUpdated) && this.keywordUpdated) { + this.goFirst(); + } + + return this; } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js index 3f5434761ba18..d4db0100db7c6 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js @@ -22,6 +22,7 @@ define([ placeholder: $t('Search by keyword'), label: $t('Keyword'), value: '', + keywordUpdated: false, previews: [], chipsProvider: 'componentType = filtersChips, ns = ${ $.ns }', statefull: { @@ -31,7 +32,8 @@ define([ value: true, previews: true, inputValue: true, - focused: true + focused: true, + keywordUpdated: true }, imports: { inputValue: 'value', @@ -39,7 +41,8 @@ define([ focused: false }, exports: { - value: '${ $.provider }:params.search' + value: '${ $.provider }:params.search', + keywordUpdated: '${ $.provider }:params.keywordUpdated' }, modules: { chips: '${ $.chipsProvider }' @@ -124,6 +127,7 @@ define([ apply: function (value) { value = value || this.inputValue; + this.keywordUpdated = this.value !== value; this.value = this.inputValue = value.trim(); return this; diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup.xml new file mode 100644 index 0000000000000..2e80fa0f798a6 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup" extends="AdminSearchByRequestPathActionGroup"> + <annotations> + <description>Goes to the Admin URL Rewrite grid page. Searches the grid based on the provided Redirect Path and StoreView name. Validates that the provided Redirect Path, Type and Target Path are present and correct in the grid.</description> + </annotations> + <arguments> + <argument name="storeView" type="string"/> + </arguments> + <selectOption selector="{{AdminDataGridHeaderSection.filterFieldSelect('store_id')}}" userInput="{{storeView}}" stepKey="fillStoreView" after="fillRedirectPathFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml index c14d0b175d2c0..ccd4297312f44 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml @@ -109,10 +109,11 @@ </actionGroup> <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> <!-- Open Url Rewrite page and verify new Redirect Path, RedirectType and Target Path for the grabbed product Id --> - <actionGroup ref="AdminSearchByRequestPathActionGroup" stepKey="searchPath1"> + <actionGroup ref="AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup" stepKey="searchPath1"> <argument name="redirectPath" value="$$createProduct.name$$.html"/> <argument name="redirectType" value="No"/> <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + <argument name="storeView" value="{{storeViewData.name}}"/> </actionGroup> <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreValueForProductId"> diff --git a/app/code/Magento/User/etc/adminhtml/system.xml b/app/code/Magento/User/etc/adminhtml/system.xml index 584b40a023c93..8b619c4f9cf48 100644 --- a/app/code/Magento/User/etc/adminhtml/system.xml +++ b/app/code/Magento/User/etc/adminhtml/system.xml @@ -14,6 +14,11 @@ <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> + <field id="new_user_notification_template" translate="label comment" type="select" sortOrder="50" showInDefault="1" canRestore="1"> + <label>New User Notification Template</label> + <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> + <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> + </field> </group> <group id="security"> <field id="lockout_failures" translate="label comment" sortOrder="100" showInDefault="1" canRestore="1"> diff --git a/app/code/Magento/User/view/adminhtml/email/new_user_notification.html b/app/code/Magento/User/view/adminhtml/email/new_user_notification.html index 87f4e4669c4b6..0b6ceeb61cb71 100644 --- a/app/code/Magento/User/view/adminhtml/email/new_user_notification.html +++ b/app/code/Magento/User/view/adminhtml/email/new_user_notification.html @@ -4,12 +4,12 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "New admin user '%user_name' created" user_name=$user.name}} @--> +<!--@subject {{trans "New admin user '%user_name' created" user_name=$user.username}} @--> <!--@vars { "var store.frontend_name":"Store Name", -"var user.name":"User Name", -"var user.first_name":"User First Name", -"var user.last_name":"User Last Name", +"var user.username":"User Name", +"var user.firstname":"User First Name", +"var user.lastname":"User Last Name", "var user.email":"User Email", "var store_email":"Store Email", "var store_phone":"Store Phone" @@ -17,7 +17,7 @@ {{trans "Hello,"}} -{{trans "A new admin account was created for %first_name, %last_name using %email." first_name=$user.first_name last_name=$user.last_name email=$user.email}} +{{trans "A new admin account was created for %first_name, %last_name using %email." first_name=$user.firstname last_name=$user.lastname email=$user.email}} {{trans "If you have not authorized this action, please contact us immediately at %store_email" store_email=$store_email |escape}}{{depend store_phone}} {{trans "or call us at %store_phone" store_phone=$store_phone |escape}}{{/depend}}. {{trans "Thanks,"}} diff --git a/app/code/Magento/Weee/Test/Mftf/Data/FixedProductAttributeData.xml b/app/code/Magento/Weee/Test/Mftf/Data/FixedProductAttributeData.xml index b8b45d84242c9..071f96bb65266 100644 --- a/app/code/Magento/Weee/Test/Mftf/Data/FixedProductAttributeData.xml +++ b/app/code/Magento/Weee/Test/Mftf/Data/FixedProductAttributeData.xml @@ -8,7 +8,7 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="productFPTAttribute" type="ProductAttribute"> + <entity name="productFPTAttribute" type="ProductAttribute" deprecated="Use FPTProductAttribute instead"> <data key="attribute_code" unique="suffix">attribute</data> <data key="is_unique">true</data> <data key="frontend_input">weee</data> @@ -17,4 +17,13 @@ <data key="is_filterable_in_grid">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="FPTProductAttribute" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">weee_attribute</data> + <data key="is_unique">true</data> + <data key="frontend_input">weee</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabelWeee</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Weee/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Weee/Test/Mftf/Data/FrontendLabelData.xml new file mode 100644 index 0000000000000..7c362ba0ee303 --- /dev/null +++ b/app/code/Magento/Weee/Test/Mftf/Data/FrontendLabelData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductAttributeFrontendLabelWeee" type="FrontendLabel"> + <data key="store_id">0</data> + <data key="label" unique="suffix">weee-attribute</data> + </entity> +</entities> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml index 0f4a7f9a55d26..ccbd431848dbc 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml @@ -22,7 +22,7 @@ <before> <!-- Create product attribute and add it to default attribute set />--> <comment userInput="Create product attribute and add it to default attribute set" stepKey="createAttrAndAddToDefaultAttrSet"/> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml index 0d7c21b6efffc..4e70c9ba87d64 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml @@ -18,7 +18,7 @@ <group value="weee"/> </annotations> <before> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml index e78036458301b..833f619888bfb 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml @@ -27,7 +27,7 @@ <!-- Tax Rule is created based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> <!-- Fixed Product Tax attribute is created and added to default attribute set --> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml index dda125835110a..8e8667cb7e13d 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml @@ -27,7 +27,7 @@ <!-- Tax Rule is created based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> <!-- Fixed Product Tax attribute is created and added to default attribute set --> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml index 74ba7c1f2bff3..3a3f9c7e8931a 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml @@ -27,7 +27,7 @@ <!-- Tax Rule is created based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> <!-- Fixed Product Tax attribute is created and added to default attribute set --> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml index 495b9a990a465..0d54991f84395 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml @@ -27,7 +27,7 @@ <!-- Tax Rule is created based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> <!-- Fixed Product Tax attribute is created and added to default attribute set --> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php index c48bf9e7e4c7a..a704a5676f632 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php @@ -315,25 +315,6 @@ public function getAddLayoutButtonHtml() return $button->toHtml(); } - /** - * Retrieve remove layout button html - * - * @return string - */ - public function getRemoveLayoutButtonHtml() - { - $button = $this->getLayout()->createBlock( - \Magento\Backend\Block\Widget\Button::class - )->setData( - [ - 'label' => $this->escapeHtmlAttr(__('Remove Layout Update')), - 'onclick' => 'WidgetInstance.removePageGroup(this)', - 'class' => 'action-delete', - ] - ); - return $button->toHtml(); - } - /** * Prepare and retrieve page groups data of widget instance * diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml index e657b3eb73b53..5cf135f158130 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -23,6 +23,10 @@ <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widget.name}}" stepKey="fillTitle"/> <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" userInput="{{widget.store_ids[0]}}" stepKey="setWidgetStoreIds"/> <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate2"/> + <seeNumberOfElements userInput="2" selector="{{AdminNewWidgetSection.layoutUpdate}}" stepKey="seeTwoLayoutUpdates"/> + <click selector="{{AdminNewWidgetSection.removeLastLayoutUpdate}}" stepKey="clickRemoveLastLayoutUpdate"/> + <seeNumberOfElements userInput="1" selector="{{AdminNewWidgetSection.layoutUpdate}}" stepKey="seeOneLayoutUpdate"/> <selectOption selector="{{AdminNewWidgetSection.selectDisplayOn}}" userInput="{{widget.display_on}}" stepKey="setDisplayOn"/> <waitForAjaxLoad stepKey="waitForLoad"/> <selectOption selector="{{AdminNewWidgetSection.selectContainer}}" userInput="{{widget.container}}" stepKey="setContainer"/> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetWthoutLayoutActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetWthoutLayoutActionGroup.xml new file mode 100644 index 0000000000000..e9ee80c1a5f2a --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetWthoutLayoutActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateWidgetWthoutLayoutActionGroup"> + <annotations> + <description>Goes to the Admin Widget creation page without saving it</description> + </annotations> + <arguments> + <argument name="widget"/> + </arguments> + <amOnPage url="{{AdminNewWidgetPage.url}}" stepKey="amOnAdminNewWidgetPage"/> + <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widget.type}}" stepKey="setWidgetType"/> + <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widget.design_theme}}" stepKey="setWidgetDesignTheme"/> + <click selector="{{AdminNewWidgetSection.continue}}" stepKey="clickContinue"/> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widget.name}}" stepKey="fillTitle"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" userInput="{{widget.store_ids[0]}}" stepKey="setWidgetStoreIds"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetAddLayoutUpdateActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetAddLayoutUpdateActionGroup.xml new file mode 100644 index 0000000000000..fa73fa4926e10 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetAddLayoutUpdateActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminWidgetAddLayoutUpdateActionGroup"> + <annotations> + <description>Add layouts during widgets creation</description> + </annotations> + <waitForAjaxLoad stepKey="waitForLoad"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetDeleteLayoutUpdateActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetDeleteLayoutUpdateActionGroup.xml new file mode 100644 index 0000000000000..e52fb1a7f6514 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetDeleteLayoutUpdateActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminWidgetDeleteLayoutUpdateActionGroup"> + <annotations> + <description>Delete layouts during widgets creation</description> + </annotations> + <click selector="{{AdminNewWidgetSection.deleteWidgetLayoutAction}}" stepKey="clickFirstDeleteButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index 8a17b589d7ab2..1ed77904d3a68 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -17,6 +17,8 @@ <element name="widgetStoreIds" type="select" selector="#store_ids"/> <element name="widgetSortOrder" type="input" selector="#sort_order"/> <element name="addLayoutUpdate" type="button" selector=".action-default.scalable.action-add"/> + <element name="layoutUpdate" type="block" selector=".page_group_container"/> + <element name="removeLastLayoutUpdate" type="button" selector=".page_group_container:last-child .action-default.scalable.action-delete"/> <element name="selectDisplayOn" type="select" selector="#widget_instance[0][page_group]"/> <element name="selectContainer" type="select" selector="#all_pages_0>table>tbody>tr>td:nth-child(1)>div>div>select"/> <element name="displayOnByIndex" type="select" selector="select[name='widget_instance[{{index}}][page_group]']" parameterized="true"/> @@ -52,6 +54,8 @@ <element name="displayPageControl" type="select" selector="[name='parameters[show_pager]']"/> <element name="numberOfProductsToDisplay" type="input" selector="[name='parameters[products_count]']"/> <element name="cacheLifetime" type="input" selector="[name='parameters[cache_lifetime]']"/> + <element name="deleteWidgetLayoutAction" type="button" selector="#page_group_container > div:first-of-type > div.fieldset-wrapper-title > div > .action-default.action-delete"/> + <element name="CountDeleteButtons" type="button" selector="#page_group_container > .fieldset-wrapper.page_group_container > div.fieldset-wrapper-title > div > .action-default.action-delete"/> </section> </sections> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml new file mode 100644 index 0000000000000..04c1552d53522 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminWidgetAddAndDeleteMultipleLayoutSectionsTest"> + <annotations> + <features value="Widget"/> + <stories value="Add and Delete multiple layouts when creating a Widget"/> + <title value="Add and Delete multiple layouts"/> + <description value="Admin should be able to Add and Delete multiple layouts"/> + <severity value="CRITICAL"/> + <group value="Widget"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentWidgetsPageFirst"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentElementsWidgets.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitleFirst"> + <argument name="title" value="{{AdminMenuContentElementsWidgets.pageTitle}}"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear1"/> + <actionGroup ref="AdminCreateWidgetWthoutLayoutActionGroup" stepKey="addWidgetForTest"> + <argument name="widget" value="ProductsListWidget"/> + </actionGroup> + <actionGroup ref="AdminWidgetAddLayoutUpdateActionGroup" stepKey="AddSecondLayout"/> + <actionGroup ref="AdminWidgetAddLayoutUpdateActionGroup" stepKey="AddThirdLayout"/> + <seeNumberOfElements userInput="3" selector="{{AdminNewWidgetSection.CountDeleteButtons}}" stepKey="seeThreeDeleteButtons"/> + <actionGroup ref="AdminWidgetDeleteLayoutUpdateActionGroup" stepKey="DeleteFirstLayoutForWidget"></actionGroup> + <seeNumberOfElements userInput="2" selector="{{AdminNewWidgetSection.CountDeleteButtons}}" stepKey="seeTwoDeleteButtons"/> + <actionGroup ref="AdminWidgetDeleteLayoutUpdateActionGroup" stepKey="DeleteSecondLayoutForWidget"></actionGroup> + <seeNumberOfElements userInput="1" selector="{{AdminNewWidgetSection.CountDeleteButtons}}" stepKey="seeOneDeleteButtons"/> + <actionGroup ref="AdminWidgetDeleteLayoutUpdateActionGroup" stepKey="DeleteThirdLayoutForWidget"></actionGroup> + <seeNumberOfElements userInput="0" selector="{{AdminNewWidgetSection.CountDeleteButtons}}" stepKey="seeZeroDeleteButtons"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml index 93c5cac33f947..2d4f88709fd91 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml @@ -38,7 +38,8 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" '<label for="widget_instance[<%- data.id %>][page_group]">Display on <span class="required">*</span></label>'+ '{$block->getDisplayOnSelectHtml()}'+ '<div class="actions">'+ - {$jsonHelper->jsonEncode($block->getRemoveLayoutButtonHtml())} + + '<button title="{$escaper->escapeHtmlAttr(__('Remove Layout Update'))}" type="button"'+ + ' class="action-default scalable action-delete" onclick="WidgetInstance.removePageGroup(this)" />'+ '</div>'+ '</div>'+ '<div class="fieldset-wrapper-content">'+ diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index aafd8b0b0d4d3..8fe3d3c707900 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -36,6 +36,8 @@ </before> <after> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> @@ -49,7 +51,7 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsFilters"/> <!--Logout everywhere--> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <comment userInput="BIC workaround" stepKey="customerLogout"/> </after> <!-- Change products visibility on store-view level --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml index 689b76e42e6f1..96e5417f1cffd 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml @@ -23,9 +23,11 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <comment userInput="BIC workaround" stepKey="customerLogout"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> diff --git a/app/etc/di.xml b/app/etc/di.xml index d4651499e6fce..b1d81ed70f6b4 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -229,6 +229,17 @@ </arguments> </type> <preference for="Magento\AsynchronousOperations\Model\ConfigInterface" type="Magento\WebapiAsync\Model\Config\Proxy" /> + <virtualType name="Magento\Framework\Communication\Config\Reader\XmlReader" type="Magento\Framework\Config\Reader\Filesystem"> + <arguments> + <argument name="converter" xsi:type="object">Magento\Framework\Communication\Config\Reader\XmlReader\Converter</argument> + <argument name="schemaLocator" xsi:type="object">Magento\Framework\Communication\Config\Reader\XmlReader\SchemaLocator</argument> + <argument name="fileName" xsi:type="string">communication.xml</argument> + <argument name="idAttributes" xsi:type="array"> + <item name="/config/topic" xsi:type="string">name</item> + <item name="/config/topic/handler" xsi:type="string">name</item> + </argument> + </arguments> + </virtualType> <type name="Magento\Framework\Communication\Config\CompositeReader"> <arguments> <argument name="readers" xsi:type="array"> diff --git a/composer.lock b/composer.lock index fa90484975728..b0b15f4e13f6c 100644 --- a/composer.lock +++ b/composer.lock @@ -8439,21 +8439,21 @@ }, { "name": "magento/magento-coding-standard", - "version": "5", + "version": "6", "source": { "type": "git", "url": "https://github.com/magento/magento-coding-standard.git", - "reference": "da46c5d57a43c950dfa364edc7f1f0436d5353a5" + "reference": "efc9084db3d1bd145b92d6b8a2e9cb0faec54fa7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/da46c5d57a43c950dfa364edc7f1f0436d5353a5", - "reference": "da46c5d57a43c950dfa364edc7f1f0436d5353a5", + "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/efc9084db3d1bd145b92d6b8a2e9cb0faec54fa7", + "reference": "efc9084db3d1bd145b92d6b8a2e9cb0faec54fa7", "shasum": "" }, "require": { "php": ">=5.6.0", - "squizlabs/php_codesniffer": "^3.4", + "squizlabs/php_codesniffer": "^3.5", "webonyx/graphql-php": ">=0.12.6 <1.0" }, "require-dev": { @@ -8474,7 +8474,7 @@ "AFL-3.0" ], "description": "A set of Magento specific PHP CodeSniffer rules.", - "time": "2019-11-04T22:08:27+00:00" + "time": "2020-12-03T14:41:54+00:00" }, { "name": "magento/magento2-functional-testing-framework", diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php index ba12a02cb5b1f..09987457e3c56 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php @@ -235,8 +235,8 @@ public function testCreateWithNotDefaultStoreId() $this->assertEquals($updatedImage['file'], $targetProduct->getData('image')); // No values for default store view were provided $this->assertNull($updatedImage['label_default']); - $this->assertNull($updatedImage['position_default']); - $this->assertNull($updatedImage['disabled_default']); + $this->assertEquals(1, $updatedImage['position_default']); + $this->assertEquals(0, $updatedImage['disabled_default']); } /** @@ -483,7 +483,9 @@ public function testCreateThrowsExceptionIfProvidedImageHasWrongMimeType() public function testCreateThrowsExceptionIfTargetProductDoesNotExist() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The product that was requested doesn\'t exist. Verify the product and try again.'); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); $this->createServiceInfo['rest']['resourcePath'] = '/V1/products/wrong_product_sku/media'; @@ -538,7 +540,9 @@ public function testCreateThrowsExceptionIfProvidedImageNameContainsForbiddenCha public function testUpdateThrowsExceptionIfTargetProductDoesNotExist() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The product that was requested doesn\'t exist. Verify the product and try again.'); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); $this->updateServiceInfo['rest']['resourcePath'] = '/V1/products/wrong_product_sku/media' . '/' . 'wrong-sku'; @@ -592,7 +596,9 @@ public function testUpdateThrowsExceptionIfThereIsNoImageWithGivenId() public function testDeleteThrowsExceptionIfTargetProductDoesNotExist() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The product that was requested doesn\'t exist. Verify the product and try again.'); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); $this->deleteServiceInfo['rest']['resourcePath'] = '/V1/products/wrong_product_sku/media/9999'; $requestData = [ @@ -782,6 +788,6 @@ public function testAddProductVideo() $this->assertEquals(1, $updatedImage['position']); $this->assertEquals(0, $updatedImage['disabled']); $this->assertStringStartsWith('/t/e/test_image', $updatedImage['file']); - $this->assertEquals($videoContent, array_intersect($updatedImage, $videoContent)); + $this->assertEquals($videoContent, array_intersect_key($updatedImage, $videoContent)); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 1b18949b0ac5b..d77737edadafa 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -25,6 +25,7 @@ use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\StoreWebsiteRelationInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreRepository; use Magento\Store\Model\Website; @@ -254,6 +255,59 @@ public function testUpdateWithDeleteWebsites() ); } + /** + * Test removing association between product and website 1 then check url rewrite removed + * Assign website back and check rewrite generated + * + * @magentoApiDataFixture Magento/Catalog/_files/product_two_websites.php + */ + public function testUpdateRewriteWithChangeWebsites() + { + /** @var Website $website */ + $website = $this->loadWebsiteByCode('test'); + + $productBuilder[ProductInterface::SKU] = 'simple-on-two-websites'; + $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = [ + 'website_ids' => [ + $website->getId(), + ], + ]; + $objectManager = Bootstrap::getObjectManager(); + /** @var StoreWebsiteRelationInterface $storeWebsiteRelation */ + $storeWebsiteRelation = $objectManager->get(StoreWebsiteRelationInterface::class); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + + $baseWebsite = $this->loadWebsiteByCode('base'); + $storeIds = $storeWebsiteRelation->getStoreByWebsiteId($baseWebsite->getId()); + $product = $productRepository->get($productBuilder[ProductInterface::SKU], false, reset($storeIds)); + $this->assertStringContainsString( + $product->getUrlKey() . '.html', + $product->getProductUrl() + ); + + $this->updateProduct($productBuilder); + + $product->setRequestPath(''); + $this->assertStringNotContainsString( + $product->getUrlKey() . '.html', + $product->getProductUrl() + ); + $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = [ + 'website_ids' => [ + $website->getId(), + $baseWebsite->getId(), + ], + ]; + + $this->updateProduct($productBuilder); + $product->setRequestPath(''); + $this->assertStringContainsString( + $product->getUrlKey() . '.html', + $product->getProductUrl() + ); + } + /** * Test removing all website associations * @@ -264,7 +318,7 @@ public function testDeleteAllWebsiteAssociations() $productBuilder[ProductInterface::SKU] = 'unique-simple-azaza'; $websitesData = [ - 'website_ids' => [] + 'website_ids' => [], ]; $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = $websitesData; $response = $this->updateProduct($productBuilder); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryAggregationsTest.php new file mode 100644 index 0000000000000..a0f184507a9aa --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryAggregationsTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog\CategoriesQuery; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test to return category aggregations + */ +class CategoryAggregationsTest extends GraphQlAbstract +{ + /** + * Test to return category aggregations in sorting by position + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attributes.php + */ + public function testCategoryAggregationSorting(): void + { + $categoryId = 3334; + $query = <<<QUERY +{ + products(filter: {category_id: {eq: "{$categoryId}"}}) { + aggregations{ + label + attribute_code + count + options{ + label + value + count + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $response); + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('aggregations', $response['products']); + + $customAggregation = array_values(array_filter( + $response['products']['aggregations'], + function ($a) { + return in_array($a['attribute_code'], ['test_attribute_1', 'test_attribute_2']); + } + )); + $this->assertCount(2, $customAggregation); + $this->assertEquals('test_attribute_2', $customAggregation[0]['attribute_code']); + $this->assertEquals('test_attribute_1', $customAggregation[1]['attribute_code']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php index b6c4b55dc1d23..ceb52354fa6eb 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php @@ -15,6 +15,8 @@ use Magento\ConfigurableProduct\Api\LinkManagementInterface; use Magento\ConfigurableProduct\Model\LinkManagement; use Magento\Customer\Model\Group; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\GraphQl\Customer\LockCustomer; use Magento\Framework\ObjectManager\ObjectManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -27,11 +29,23 @@ class ProductPriceTest extends GraphQlAbstract /** @var ProductRepositoryInterface $productRepository */ private $productRepository; + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var LockCustomer + */ + private $lockCustomer; + protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); /** @var ProductRepositoryInterface $productRepository */ $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + $this->lockCustomer = $this->objectManager->get(LockCustomer::class); } /** @@ -235,10 +249,20 @@ public function testMultipleProductTypes() * Simple products with special price and tier price with % discount * * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @param int $customerGroup + * @param array $expectedPriceRange + * @param array $expectedTierPrices + * @param array $customerData + * @param bool $isTierPriceExists + * @dataProvider priceDataProvider */ - public function testSimpleProductsWithSpecialPriceAndTierPrice() - { + public function testSimpleProductsWithSpecialPriceAndTierPrice( + int $customerGroup, + array $expectedPriceRange, + array $expectedTierPrices, + array $customerData + ) { $skus = ["simple1", "simple2"]; $tierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); @@ -249,7 +273,7 @@ public function testSimpleProductsWithSpecialPriceAndTierPrice() $tierPrices[] = $tierPriceFactory->create( [ 'data' => [ - 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'customer_group_id' => $customerGroup, 'qty' => 2 ] ] @@ -260,97 +284,137 @@ public function testSimpleProductsWithSpecialPriceAndTierPrice() $simpleProduct->setTierPrices($tierPrices); $this->productRepository->save($simpleProduct); } + + $headerMap = []; + if (!empty($customerData)) { + $customerToken = $this->customerTokenService->createCustomerAccessToken( + $customerData['username'], + $customerData['password'] + ); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + } + $query = $this->getProductQuery($skus); - $result = $this->graphQlQuery($query); + $result = $this->graphQlQuery($query, [], '', $headerMap); $this->assertArrayNotHasKey('errors', $result); $this->assertCount(2, $result['products']['items']); - $expectedPriceRange = [ - "simple1" => [ - "minimum_price" => [ - "regular_price" => [ - "value" => 10 - ], - "final_price" => [ - "value" => 5.99 + foreach ($result['products']['items'] as $product) { + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + $this->assertPrices($expectedPriceRange[$product['sku']], $product['price_range']); + $this->assertResponseFields($product['price_tiers'], $expectedTierPrices[$product['sku']]); + } + } + + /** + * Data provider for prices + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function priceDataProvider() : array + { + return [ + [ + 'customer_group' => Group::CUST_GROUP_ALL, + 'expected_price_range' => [ + "simple1" => [ + "minimum_price" => [ + "regular_price" => ["value" => 10], + "final_price" => ["value" => 5.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 40.1] + ], + "maximum_price" => [ + "regular_price" => ["value" => 10], + "final_price" => ["value" => 5.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 40.1] + ] ], - "discount" => [ - "amount_off" => 4.01, - "percent_off" => 40.1 + "simple2" => [ + "minimum_price" => [ + "regular_price" => ["value" => 20], + "final_price" => ["value" => 15.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 20.05] + ], + "maximum_price" => [ + "regular_price" => ["value" => 20], + "final_price" => ["value" => 15.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 20.05] + ] ] ], - "maximum_price" => [ - "regular_price" => [ - "value" => 10 + 'expected_tier_prices' => [ + "simple1" => [ + 0 => [ + 'discount' =>['amount_off' => 1, 'percent_off' => 10], + 'final_price' =>['value'=> 9], + 'quantity' => 2 + ] ], - "final_price" => [ - "value" => 5.99 - ], - "discount" => [ - "amount_off" => 4.01, - "percent_off" => 40.1 + "simple2" => [ + 0 => [ + 'discount' =>['amount_off' => 2, 'percent_off' => 10], + 'final_price' =>['value'=> 18], + 'quantity' => 2 + ] ] - ] + ], + 'customer_data' => [] ], - "simple2" => [ - "minimum_price" => [ - "regular_price" => [ - "value" => 20 - ], - "final_price" => [ - "value" => 15.99 + [ + 'customer_group' => 1, + 'expected_price_range' => [ + "simple1" => [ + "minimum_price" => [ + "regular_price" => ["value" => 10], + "final_price" => ["value" => 5.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 40.1] + ], + "maximum_price" => [ + "regular_price" => ["value" => 10], + "final_price" => ["value" => 5.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 40.1] + ] ], - "discount" => [ - "amount_off" => 4.01, - "percent_off" => 20.05 + "simple2" => [ + "minimum_price" => [ + "regular_price" => ["value" => 20], + "final_price" => ["value" => 15.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 20.05] + ], + "maximum_price" => [ + "regular_price" => ["value" => 20], + "final_price" => ["value" => 15.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 20.05] + ] ] ], - "maximum_price" => [ - "regular_price" => [ - "value" => 20 + 'expected_tier_prices' => [ + "simple1" => [ + 0 => [ + 'discount' =>['amount_off' => 1, 'percent_off' => 10], + 'final_price' =>['value'=> 9], + 'quantity' => 2 + ] ], - "final_price" => [ - "value" => 15.99 - ], - "discount" => [ - "amount_off" => 4.01, - "percent_off" => 20.05 + "simple2" => [ + 0 => [ + 'discount' =>['amount_off' => 2, 'percent_off' => 10], + 'final_price' =>['value'=> 18], + 'quantity' => 2 + ] ] - ] - ] - ]; - $expectedTierPrices = [ - "simple1" => [ - 0 => [ - 'discount' =>[ - 'amount_off' => 1, - 'percent_off' => 10 - ], - 'final_price' =>['value'=> 9], - 'quantity' => 2 + ], + 'customer_data' => [ + 'username' => 'customer@example.com', + 'password' => 'password' ] ], - "simple2" => [ - 0 => [ - 'discount' =>[ - 'amount_off' => 2, - 'percent_off' => 10 - ], - 'final_price' =>['value'=> 18], - 'quantity' => 2 - ] - - ] ]; - - foreach ($result['products']['items'] as $product) { - $this->assertNotEmpty($product['price_range']); - $this->assertNotEmpty($product['price_tiers']); - $this->assertPrices($expectedPriceRange[$product['sku']], $product['price_range']); - $this->assertResponseFields($product['price_tiers'], $expectedTierPrices[$product['sku']]); - } } + /** * Check the pricing for a grouped product with simple products having special price set * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 1da0a7e08332b..6c64539e38cb2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -173,7 +173,6 @@ private function compareFilterNames(array $a, array $b) */ public function testLayeredNavigationForConfigurableProducts() { - CacheCleaner::cleanAll(); $attributeCode = 'test_configurable'; /** @var Config $eavConfig */ @@ -277,7 +276,7 @@ private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $fi */ public function testFilterProductsByDropDownCustomAttribute() { - CacheCleaner::cleanAll(); + CacheCleaner::clean(['eav']); $attributeCode = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attributeCode); $query = <<<QUERY diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php index 518ec91be3ff0..e6c7851f775df 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php @@ -219,12 +219,10 @@ public function testRedirectsAndCustomInput() $urlRewriteModel->setRedirectType('301'); $urlRewriteModel->setId($urlRewriteModel->getId()); $urlRewriteModel->save(); - - ObjectManager::getInstance()->get(\Magento\TestFramework\Helper\CacheCleaner::class)->cleanAll(); //modifying query by adding spaces to avoid getting cached values. $this->queryUrlAndAssertResponse( (int) $product->getEntityId(), - $customUrl, + $customUrl . ' ', $actualUrls->getRequestPath(), strtoupper($actualUrls->getEntityType()), 301 diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php index 72d35fdd51b96..db9a12e654a2c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -327,6 +327,45 @@ public function testPlaceOrderOfCustomerCart() $this->graphQlMutation($query); } + /** + * Test place order with gift message options + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoConfigFixture default_store carriers/flatrate/active 1 + * @magentoConfigFixture default_store carriers/tablerate/active 1 + * @magentoConfigFixture default_store carriers/freeshipping/active 1 + * @magentoConfigFixture default_store payment/banktransfer/active 1 + * @magentoConfigFixture default_store payment/cashondelivery/active 1 + * @magentoConfigFixture default_store payment/checkmo/active 1 + * @magentoConfigFixture default_store payment/purchaseorder/active 1 + * @magentoConfigFixture sales/gift_options/allow_order 1 + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 1 + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_gift_options.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrderWithGiftMessage() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); + $orderIncrementId = $response['placeOrder']['order']['order_number']; + $order = $this->orderFactory->create(); + $order->loadByIncrementId($orderIncrementId); + $this->assertNotEmpty($order->getGiftMessageId()); + } + /** * @param string $maskedQuoteId * @return string diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php index 7a14aca72d83f..e3c77a40a470a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php @@ -8,6 +8,9 @@ namespace Magento\GraphQl\Quote\Guest; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -21,10 +24,22 @@ class SetGuestEmailOnCartTest extends GraphQlAbstract */ private $getMaskedQuoteIdByReservedOrderId; + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); } /** @@ -43,6 +58,33 @@ public function testSetGuestEmailOnCart() $this->assertEquals($email, $response['setGuestEmailOnCart']['cart']['email']); } + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testSetGuestEmailOnCartWithDifferentEmailAddress() + { + $reservedOrderId = 'test_quote'; + $secondEmail = 'attempt2@example.com'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $email = 'attempt1@example.com'; + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query); + + $query = $this->getQuery($maskedQuoteId, $secondEmail); + $this->graphQlMutation($query); + + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $addresses = $quote->getAddressesCollection(); + $this->assertEquals(2, $addresses->count()); + foreach ($addresses as $address) { + if ($address->getAddressType() === Address::ADDRESS_TYPE_SHIPPING) { + $this->assertEquals($secondEmail, $address->getEmail()); + } + } + } + /** * _security * @magentoApiDataFixture Magento/Customer/_files/customer.php diff --git a/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php index 11e07d081636e..efa7341c36a40 100644 --- a/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php @@ -7,28 +7,44 @@ namespace Magento\GroupedProduct\Api; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Indexer\Model\Config; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Webapi\Rest\Request; class ProductLinkRepositoryTest extends \Magento\TestFramework\TestCase\WebapiAbstract { const SERVICE_NAME = 'catalogProductLinkRepositoryV1'; const SERVICE_VERSION = 'V1'; const RESOURCE_PATH = '/V1/products/'; + const SERVICE_NAME_SEARCH = 'searchV1'; + const RESOURCE_PATH_SEARCH = '/V1/search/'; /** * @var \Magento\Framework\ObjectManagerInterface */ protected $objectManager; + /** + * @var array + */ + private $indexersState; + + /** + * @var mixed + */ + private $indexerRegistry; + protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); + $this->indexerRegistry = $this->objectManager->get(IndexerRegistry::class); } /** * @magentoApiDataFixture Magento/Catalog/_files/product_simple_duplicated.php * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped.php */ - public function testSave() + public function testSave(): void { $productSku = 'grouped-product'; $linkType = 'associated'; @@ -46,7 +62,7 @@ public function testSave() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . $productSku . '/links', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -64,4 +80,106 @@ public function testSave() }); $this->assertEquals($productData, $actual[2]); } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped.php + */ + public function testLinkWithScheduledIndex(): void + { + $this->setIndexScheduled(); + $productSkuGrouped = 'grouped-product'; + $productSimple = 'simple-1'; + $linkType = 'associated'; + $productData = [ + 'sku' => $productSkuGrouped, + 'link_type' => $linkType, + 'linked_product_type' => 'simple', + 'linked_product_sku' => $productSimple, + 'position' => 3, + 'extension_attributes' => [ + 'qty' => (float) 300.0000, + ], + ]; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . $productSkuGrouped . '/links', + 'httpMethod' => Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + $this->_webApiCall($serviceInfo, ['entity' => $productData]); + + $searchCriteria = $this->buildSearchCriteria($productSimple); + $serviceInfo = $this->buildSearchServiceInfo($searchCriteria); + $response = $this->_webApiCall($serviceInfo, $searchCriteria); + $this->assertArrayHasKey('search_criteria', $response); + $this->assertArrayHasKey('items', $response); + $this->assertGreaterThan(1, count($response['items'])); + $this->assertGreaterThan(0, $response['items'][0]['id']); + $this->restoreIndexMode(); + } + + /** + * @param string $productSku + * @return array + */ + private function buildSearchCriteria(string $productSku): array + { + return [ + 'searchCriteria' => [ + 'request_name' => 'quick_search_container', + 'filter_groups' => [ + [ + 'filters' => [ + [ + 'field' => 'search_term', + 'value' => $productSku, + ] + ] + ] + ] + ] + ]; + } + + /** + * @param array $searchCriteria + * @return array + */ + private function buildSearchServiceInfo(array $searchCriteria): array + { + return [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH_SEARCH . '?' . http_build_query($searchCriteria), + 'httpMethod' => Request::HTTP_METHOD_GET + ], + 'soap' => [ + 'service' => self::SERVICE_NAME_SEARCH, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME_SEARCH . 'Search' + ] + ]; + } + + private function setIndexScheduled(): void + { + $indexerListIds = $this->objectManager->get(Config::class)->getIndexers(); + foreach ($indexerListIds as $indexerId) { + $indexer = $this->indexerRegistry->get($indexerId['indexer_id']); + $this->indexersState[$indexerId['indexer_id']] = $indexer->isScheduled(); + $indexer->setScheduled(true); + } + } + + private function restoreIndexMode(): void + { + foreach ($this->indexersState as $indexerId => $state) { + $this->indexerRegistry->get($indexerId)->setScheduled($state); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartAddingItemsTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartAddingItemsTest.php index 7900ae45e2f3d..bb2a4e68212cf 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartAddingItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartAddingItemsTest.php @@ -7,6 +7,11 @@ namespace Magento\Quote\Api; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -15,13 +20,22 @@ class CartAddingItemsTest extends WebapiAbstract { /** - * @var \Magento\TestFramework\ObjectManager + * @var ObjectManager */ protected $objectManager; + /** + * @var ProductResource + */ + private $productResource; + + /** + * @inheritDoc + */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productResource = $this->objectManager->get(ProductResource::class); } /** @@ -36,9 +50,9 @@ public function testPriceForCreatingQuoteFromEmptyCart() $this->_markTestAsRestOnly(); // Get customer ID token - /** @var \Magento\Integration\Api\CustomerTokenServiceInterface $customerTokenService */ + /** @var CustomerTokenServiceInterface $customerTokenService */ $customerTokenService = $this->objectManager->create( - \Magento\Integration\Api\CustomerTokenServiceInterface::class + CustomerTokenServiceInterface::class ); $token = $customerTokenService->createCustomerAccessToken( 'customer_one_address@test.com', @@ -49,7 +63,7 @@ public function testPriceForCreatingQuoteFromEmptyCart() $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/carts/mine', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, 'token' => $token ] ]; @@ -58,13 +72,6 @@ public function testPriceForCreatingQuoteFromEmptyCart() $this->assertGreaterThan(0, $quoteId); // Adding item to the cart - $serviceInfoForAddingProduct = [ - 'rest' => [ - 'resourcePath' => '/V1/carts/mine/items', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, - 'token' => $token - ] - ]; $requestData = [ 'cartItem' => [ 'quote_id' => $quoteId, @@ -72,7 +79,7 @@ public function testPriceForCreatingQuoteFromEmptyCart() 'qty' => 1 ] ]; - $item = $this->_webApiCall($serviceInfoForAddingProduct, $requestData); + $item = $this->_webApiCall($this->getServiceInfoAddToCart($token), $requestData); $this->assertNotEmpty($item); $this->assertEquals(10, $item['price']); @@ -80,7 +87,7 @@ public function testPriceForCreatingQuoteFromEmptyCart() $serviceInfoForGettingPaymentInfo = [ 'rest' => [ 'resourcePath' => '/V1/carts/mine/payment-information', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, 'token' => $token ] ]; @@ -92,4 +99,137 @@ public function testPriceForCreatingQuoteFromEmptyCart() $quote->load($quoteId); $quote->delete(); } + + /** + * Test qty for cart after adding grouped product with custom qty. + * + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer_one_address.php + * @return void + */ + public function testAddToCartGroupedCustomQuantity(): void + { + $this->_markTestAsRestOnly(); + + $firstProductId = $this->productResource->getIdBySku('simple_11'); + $secondProductId = $this->productResource->getIdBySku('simple_22'); + $qtyData = [$firstProductId => 2, $secondProductId => 4]; + + // Get customer ID token + /** @var CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = $this->objectManager->create(CustomerTokenServiceInterface::class); + $token = $customerTokenService->createCustomerAccessToken( + 'customer_one_address@test.com', + 'password' + ); + + // Creating empty cart for registered customer. + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/carts/mine', + 'httpMethod' => Request::HTTP_METHOD_POST, + 'token' => $token + ] + ]; + + $quoteId = $this->_webApiCall($serviceInfo, ['customerId' => 999]); // customerId 999 will get overridden + $this->assertGreaterThan(0, $quoteId); + + // Adding item to the cart + $productOptionData = [ + 'extension_attributes' => [ + 'grouped_options' => [ + ['id' => $firstProductId, 'qty' => $qtyData[$firstProductId]], + ['id' => $secondProductId, 'qty' => $qtyData[$secondProductId]], + ] + ] + ]; + $requestData = [ + 'cartItem' => [ + 'quote_id' => $quoteId, + 'sku' => 'grouped', + 'qty' => 1, + 'product_option' => $productOptionData + ] + ]; + $response = $this->_webApiCall($this->getServiceInfoAddToCart($token), $requestData); + $this->assertArrayHasKey('product_option', $response); + $this->assertEquals($response['product_option'], $productOptionData); + + /** @var CartRepositoryInterface $cartRepository */ + $cartRepository = $this->objectManager->get(CartRepositoryInterface::class); + $quote = $cartRepository->get($quoteId); + + foreach ($quote->getAllItems() as $item) { + $this->assertEquals($qtyData[$item->getProductId()], $item->getQty()); + } + } + + /** + * Test adding grouped product when qty for grouped_options not specified. + * + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer_one_address.php + * @return void + */ + public function testAddToCartGroupedCustomQuantityNotAllParamsSpecified(): void + { + $this->_markTestAsRestOnly(); + + $productId = $this->productResource->getIdBySku('simple_11'); + + // Get customer ID token + /** @var CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = $this->objectManager->create(CustomerTokenServiceInterface::class); + $token = $customerTokenService->createCustomerAccessToken( + 'customer_one_address@test.com', + 'password' + ); + + // Creating empty cart for registered customer. + $serviceInfo = [ + 'rest' => ['resourcePath' => '/V1/carts/mine', 'httpMethod' => Request::HTTP_METHOD_POST, 'token' => $token] + ]; + + $quoteId = $this->_webApiCall($serviceInfo, ['customerId' => 999]); // customerId 999 will get overridden + $this->assertGreaterThan(0, $quoteId); + + // Adding item to the cart + $requestData = [ + 'cartItem' => [ + 'quote_id' => $quoteId, + 'sku' => 'grouped', + 'qty' => 1, + 'product_option' => [ + 'extension_attributes' => [ + 'grouped_options' => [ + ['id' => $productId], + ] + ] + ] + ] + ]; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Please specify id and qty for grouped options.'); + + $this->_webApiCall($this->getServiceInfoAddToCart($token), $requestData); + } + + /** + * Returns service info add to cart + * + * @param string $token + * @return array + */ + private function getServiceInfoAddToCart(string $token): array + { + return [ + 'rest' => [ + 'resourcePath' => '/V1/carts/mine/items', + 'httpMethod' => Request::HTTP_METHOD_POST, + 'token' => $token + ] + ]; + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php index 373ad64ba39d4..1054706819e95 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php @@ -5,8 +5,13 @@ */ namespace Magento\Quote\Api; +use Magento\Catalog\Model\Product; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\Stock; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -40,14 +45,14 @@ protected function setUp(): void */ public function testGetList() { - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $cartId = $quote->getId(); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id @@ -92,17 +97,17 @@ public function testGetList() */ public function testAddItem() { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class)->load(2); + /** @var Product $product */ + $product = $this->objectManager->create(Product::class)->load(2); $productSku = $product->getSku(); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_1', 'reserved_order_id'); $cartId = $quote->getId(); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id @@ -141,20 +146,20 @@ public function testAddItem() */ public function testRemoveItem() { - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $cartId = $quote->getId(); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id $cartId = $quoteIdMask->getMaskedId(); - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); + $product = $this->objectManager->create(Product::class); $productId = $product->getIdBySku('simple_one'); $product->load($productId); $itemId = $quote->getItemByProduct($product)->getId(); @@ -175,7 +180,7 @@ public function testRemoveItem() "itemId" => $itemId, ]; $this->assertTrue($this->_webApiCall($serviceInfo, $requestData)); - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $this->assertFalse($quote->hasProductId($productId)); } @@ -189,20 +194,20 @@ public function testRemoveItem() public function testUpdateItem(array $stockData, string $errorMessage = null) { $this->updateStockData('simple_one', $stockData); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $cartId = $quote->getId(); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id $cartId = $quoteIdMask->getMaskedId(); - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); + $product = $this->objectManager->create(Product::class); $productId = $product->getIdBySku('simple_one'); $product->load($productId); $itemId = $quote->getItemByProduct($product)->getId(); @@ -229,7 +234,7 @@ public function testUpdateItem(array $stockData, string $errorMessage = null) $this->expectExceptionMessage($errorMessage); } $this->_webApiCall($serviceInfo, $requestData); - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $this->assertTrue($quote->hasProductId(1)); $item = $quote->getItemByProduct($product); @@ -237,6 +242,62 @@ public function testUpdateItem(array $stockData, string $errorMessage = null) $this->assertEquals($itemId, $item->getItemId()); } + /** + * Verifies that store id for quote and quote item is being changed accordingly to the requested store code + * + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + */ + public function testUpdateItemWithChangingStoreId() + { + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->load('test_order_item_with_items', 'reserved_order_id'); + $cartId = $quote->getId(); + + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cartId, 'quote_id'); + $cartId = $quoteIdMask->getMaskedId(); + + $product = $this->objectManager->create(Product::class); + $productId = $product->getIdBySku('simple'); + $product->load($productId); + $itemId = $quote->getItemByProduct($product)->getId(); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . $cartId . '/items/' . $itemId, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + + $requestData['cartItem']['qty'] = 5; + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $requestData['cartItem'] += [ + 'quote_id' => $cartId, + 'itemId' => $itemId, + ]; + } + $this->_webApiCall($serviceInfo, $requestData, null, 'fixture_second_store'); + $quote = $this->objectManager->create(Quote::class); + $quote->load('test_order_item_with_items', 'reserved_order_id'); + $this->assertTrue($quote->hasProductId(1)); + $item = $quote->getItemByProduct($product); + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeId = $storeManager->getStore('fixture_second_store') + ->getId(); + $this->assertEquals($storeId, $quote->getStoreId()); + $this->assertEquals($storeId, $item->getStoreId()); + } + /** * @return array */ diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml new file mode 100644 index 0000000000000..9808d90ace49c --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleMview"/> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml b/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml new file mode 100644 index 0000000000000..1cabda7a626bf --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Mview/etc/mview.xsd"> + <view id="test_view_with_additional_columns" class="Magento\Framework\Indexer\Action\Dummy" group="indexer"> + <subscriptions> + <table name="test_mview_table" entity_column="entity_id"> + <additionalColumns> + <column name="additional_column" cl_name="test_additional_column" /> + </additionalColumns> + </table> + </subscriptions> + </view> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/registration.php b/dev/tests/integration/_files/Magento/TestModuleMview/registration.php new file mode 100644 index 0000000000000..5c5453c1bd413 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMview/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleMview') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleMview', __DIR__); +} diff --git a/dev/tests/integration/testsuite/Magento/Backend/App/Area/FrontNameResolverTest.php b/dev/tests/integration/testsuite/Magento/Backend/App/Area/FrontNameResolverTest.php new file mode 100644 index 0000000000000..979e8db19efb9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/App/Area/FrontNameResolverTest.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backend\App\Area; + +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * @magentoAppArea adminhtml + */ +class FrontNameResolverTest extends TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + protected $objectManager; + + /** + * @var FrontNameResolver + */ + protected $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create( + FrontNameResolver::class + ); + $_SERVER['HTTP_HOST'] = 'localhost'; + } + + /** + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store web/unsecure/base_url http://example.com/ + */ + public function testIsHostBackend() + { + $this->assertTrue($this->model->isHostBackend()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php index 7e94484961f9e..56a07034bd490 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php @@ -6,15 +6,20 @@ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\UpdateHandler; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Registry; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; /** * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ContentTest extends \PHPUnit\Framework\TestCase { @@ -35,6 +40,16 @@ class ContentTest extends \PHPUnit\Framework\TestCase */ private $dataPersistor; + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @inheritdoc */ @@ -51,6 +66,8 @@ protected function setUp(): void $this->block->setElement($gallery); $this->registry = Bootstrap::getObjectManager()->get(Registry::class); $this->dataPersistor = Bootstrap::getObjectManager()->get(DataPersistorInterface::class); + $this->storeRepository = Bootstrap::getObjectManager()->create(StoreRepositoryInterface::class); + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); } public function testGetUploader() @@ -120,6 +137,119 @@ public function getImagesAndImageTypesDataProvider() ]; } + /** + * Tests images positions in store view + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @dataProvider imagesPositionStoreViewDataProvider + * @param string $addFromStore + * @param array $newImages + * @param string $viewFromStore + * @param array $expectedImages + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testImagesPositionStoreView( + string $addFromStore, + array $newImages, + string $viewFromStore, + array $expectedImages + ): void { + $storeId = (int)$this->storeRepository->get($addFromStore)->getId(); + $product = $this->getProduct($storeId); + $images = $product->getData('media_gallery')['images']; + $images = array_merge($images, $newImages); + $product->setData('media_gallery', ['images' => $images]); + $updateHandler = Bootstrap::getObjectManager()->create(UpdateHandler::class); + $updateHandler->execute($product); + $storeId = (int)$this->storeRepository->get($viewFromStore)->getId(); + $product = $this->getProduct($storeId); + $this->registry->register('current_product', $product); + $actualImages = array_map( + function ($item) { + return [ + 'file' => $item['file'], + 'label' => $item['label'], + 'position' => $item['position'], + ]; + }, + json_decode($this->block->getImagesJson(), true) + ); + $this->assertEquals($expectedImages, array_values($actualImages)); + } + + /** + * @return array[] + */ + public function imagesPositionStoreViewDataProvider(): array + { + return [ + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'default', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'file' => '/m/a/magento_small_image.jpg', + 'label' => null, + 'position' => 2, + ], + ] + ], + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'file' => '/m/a/magento_small_image.jpg', + 'label' => 'New Image Alt Text', + 'position' => 2, + ], + ] + ] + ]; + } + + /** + * Returns product for testing. + * + * @param int $storeId + * @param string $sku + * @return ProductInterface + */ + private function getProduct(int $storeId = Store::DEFAULT_STORE_ID, string $sku = 'simple'): ProductInterface + { + return $this->productRepository->get($sku, false, $storeId, true); + } + /** * Prepare product, and set it to registry and data persistor. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php index b57969280cdf3..bcec3168c7885 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Gallery\UpdateHandler; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\View\LayoutInterface; @@ -392,6 +393,107 @@ public function galleryImagesOnStoreViewDataProvider(): array ]; } + /** + * Tests images positions in store view + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/web/url/catalog_media_url_format image_optimization_parameters + * @dataProvider imagesPositionStoreViewDataProvider + * @param string $addFromStore + * @param array $newImages + * @param string $viewFromStore + * @param array $expectedImages + * @return void + */ + public function testImagesPositionStoreView( + string $addFromStore, + array $newImages, + string $viewFromStore, + array $expectedImages + ): void { + $storeId = (int)$this->storeRepository->get($addFromStore)->getId(); + $product = $this->getProduct($storeId); + $images = $product->getData('media_gallery')['images']; + $images = array_merge($images, $newImages); + $product->setData('media_gallery', ['images' => $images]); + $updateHandler = Bootstrap::getObjectManager()->create(UpdateHandler::class); + $updateHandler->execute($product); + $storeId = (int)$this->storeRepository->get($viewFromStore)->getId(); + $product = $this->getProduct($storeId); + $this->block->setData('product', $product); + $actualImages = array_map( + function ($item) { + return [ + 'img' => parse_url($item['img'], PHP_URL_PATH), + 'caption' => $item['caption'], + 'position' => $item['position'], + ]; + }, + $this->serializer->unserialize($this->block->getGalleryImagesJson()) + ); + $this->assertEquals($expectedImages, array_values($actualImages)); + } + + /** + * @return array[] + */ + public function imagesPositionStoreViewDataProvider(): array + { + return [ + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'default', + [ + [ + 'img' => '/media/catalog/product/m/a/magento_image.jpg', + 'caption' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'img' => '/media/catalog/product/m/a/magento_small_image.jpg', + 'caption' => 'Simple Product', + 'position' => 2, + ], + ] + ], + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'fixture_second_store', + [ + [ + 'img' => '/media/catalog/product/m/a/magento_image.jpg', + 'caption' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'img' => '/media/catalog/product/m/a/magento_small_image.jpg', + 'caption' => 'New Image Alt Text', + 'position' => 2, + ], + ] + ] + ]; + } + /** * Updates product gallery images and saves product. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/Type/DateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/Type/DateTest.php new file mode 100644 index 0000000000000..91a54d8fc13fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/Type/DateTest.php @@ -0,0 +1,324 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\View\Options\Type; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Pricing\Render; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ +class DateTest extends TestCase +{ + /** + * @var Date + */ + private $block; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @var ProductHelper + */ + private $productHelper; + + /** + * @var ResolverInterface + */ + private $localeResolver; + + /** + * @var string + */ + private $defaultLocale; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->productHelper = $objectManager->get(ProductHelper::class); + $this->dataObjectFactory = $objectManager->get(DataObjectFactory::class); + $layout = $objectManager->get(LayoutInterface::class); + $this->localeResolver = $objectManager->get(ResolverInterface::class); + $this->defaultLocale = $this->localeResolver->getLocale(); + $this->block = $layout->createBlock( + Date::class, + 'product.info.options.date', + [ + 'data' => [ + 'template' => 'Magento_Catalog::product/view/options/type/date.phtml' + ] + ] + ); + $layout->createBlock( + Render::class, + 'product.price.render.default', + [ + 'data' => [ + 'price_render_handle' => 'catalog_product_prices', + 'use_link_for_as_low_as' => true, + ], + ] + ); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->localeResolver->setLocale($this->defaultLocale); + parent::tearDown(); + } + + /** + * @magentoAppArea frontend + * @param array $data + * @param array $expected + * @dataProvider toHtmlWithDropDownDataProvider + */ + public function testToHtmlWithDropDown(array $data, array $expected): void + { + $this->prepareBlock($data); + $this->assertXPaths($expected); + } + + /** + * @magentoAppArea frontend + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @param array $data + * @param array $expected + * @param string|null $locale + * @dataProvider toHtmlWithCalendarDataProvider + */ + public function testToHtmlWithCalendar(array $data, array $expected, ?string $locale = null): void + { + if ($locale) { + $this->localeResolver->setLocale($locale); + } + $this->prepareBlock($data); + $this->assertXPaths($expected); + } + + /** + * @param array $expected + */ + private function assertXPaths(array $expected): void + { + $html = $this->block->toHtml(); + $domXpath = new \DOMXPath($this->getHtmlDocument($html)); + foreach ($expected as $xpath => $value) { + $xpath = strtr($xpath, ['{id}' => $this->block->getOption()->getId()]); + $nodes = $domXpath->query(strtr($xpath, ['{id}' => $this->block->getOption()->getId()])); + $this->assertEquals(1, $nodes->count(), 'Cannot find element \'' . $xpath . '"\' in the HTML'); + $this->assertEquals($value, $nodes->item(0)->getAttribute('value')); + } + } + + /** + * @param array $data + */ + private function prepareBlock(array $data): void + { + /** @var Product $product */ + $product = $this->productRepository->get('simple'); + $this->block->setProduct($product); + $option = $this->getDateTimeOption($product); + $this->block->setOption($option); + $buyRequest = $this->dataObjectFactory->create(); + $buyRequest->setData( + [ + 'qty' => 1, + 'options' => [ + $option->getId() => $data + ], + ] + ); + $this->productHelper->prepareProductOptions($product, $buyRequest); + } + + /** + * @param Product $product + * @return Option|null + */ + private function getDateTimeOption(Product $product): ?Option + { + $option = null; + foreach ($product->getOptions() as $customOption) { + if ($customOption->getType() === Option::OPTION_TYPE_DATE_TIME) { + $option = $customOption; + break; + } + } + return $option; + } + + /** + * @param string $source + * @return \DOMDocument + */ + private function getHtmlDocument(string $source): \DOMDocument + { + $page =<<<HTML +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Title + + +$source + + +HTML; + $domDocument = new \DOMDocument('1.0', 'UTF-8'); + libxml_use_internal_errors(true); + $domDocument->loadHTML($page); + libxml_use_internal_errors(false); + return $domDocument; + } + + /** + * @return array + */ + public function toHtmlWithDropDownDataProvider(): array + { + return [ + [ + [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//select[@id="options_{id}_year"]/option[@selected]' => '2020', + '//select[@id="options_{id}_month"]/option[@selected]' => '3', + '//select[@id="options_{id}_day"]/option[@selected]' => '5', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ] + ], + [ + [ + 'date' => '09/30/2022', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//select[@id="options_{id}_year"]/option[@selected]' => '2020', + '//select[@id="options_{id}_month"]/option[@selected]' => '9', + '//select[@id="options_{id}_day"]/option[@selected]' => '30', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ] + ] + ]; + } + + /** + * @return array + */ + public function toHtmlWithCalendarDataProvider(): array + { + return [ + [ + [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//input[@id="options_{id}_date"]' => '3/5/2020', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ] + ], + [ + [ + 'date' => '09/30/2022', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//input[@id="options_{id}_date"]' => '9/30/2020', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ] + ], + [ + [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//input[@id="options_{id}_date"]' => '05/03/2020', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ], + 'fr_FR' + ], + [ + [ + 'date' => '09/30/2022', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//input[@id="options_{id}_date"]' => '30/09/2020', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ], + 'fr_FR' + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index 7032199e9fc4c..8b18f89542494 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -138,9 +138,6 @@ public function testSaveActionAndDuplicateWithUrlPathAttribute() $urlPathAttribute = $product->getCustomAttribute('url_path'); $this->assertEquals($urlPathAttribute->getValue(), $product->getSku()); - // clean cache - CacheCleaner::cleanAll(); - // dispatch Save&Duplicate action and check it $this->assertSaveAndDuplicateAction($product); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ConfigTest.php index 36379adcee601..8320ef979180f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ConfigTest.php @@ -31,7 +31,6 @@ protected function setUp(): void public function testGetEntityAttributeCodes() { $entityType = 'catalog_product'; - CacheCleaner::cleanAll(); $this->assertEquals( $this->config->getEntityAttributeCodes($entityType), $this->config->getEntityAttributeCodes($entityType) @@ -42,7 +41,6 @@ public function testGetAttribute() { $entityType = 'catalog_product'; $attributeCode = 'color'; - CacheCleaner::cleanAll(); $this->assertEquals( $this->config->getAttribute($entityType, $attributeCode), $this->config->getAttribute($entityType, $attributeCode) @@ -52,7 +50,6 @@ public function testGetAttribute() public function testGetEntityType() { $entityType = 'catalog_product'; - CacheCleaner::cleanAll(); $this->assertEquals( $this->config->getEntityType($entityType), $this->config->getEntityType($entityType) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php index 8e11efa8790cf..5cfa07cf5d402 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php @@ -245,36 +245,6 @@ public function testCatalogCategoryProductIndexInvalidateAfterDelete(): void $this->assertEquals(StateInterface::STATUS_INVALID, $status); } - /** - * Test invalidate reindex after change product position on category - * - * @magentoAppArea adminhtml - * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php - * - * @return void - */ - public function testCatalogCategoryProductIndexInvalidateAfterChangeProductPosition(): void - { - $this->indexer->setScheduled(true); - $indexerShouldBeValid = $this->indexer->isValid(); - - $category = $this->getCategoryByName->execute('Category 999'); - - $category->setPostedProducts([ - $this->productResource->getIdBySku('simple1000') => 1, - $this->productResource->getIdBySku('simple1001') => 2 - ]); - - $this->categoryResource->save($category); - - $state = $this->indexer->getState(); - $state->loadByIndexer($this->indexer->getId()); - $status = $state->getStatus(); - - $this->assertTrue($indexerShouldBeValid); - $this->assertEquals(StateInterface::STATUS_INVALID, $status); - } - /** * @param int $count * @return Category[] diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Source/CountryofmanufactureTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Source/CountryofmanufactureTest.php index 33e82e9f6ddcc..8b8f54af2d387 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Source/CountryofmanufactureTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Source/CountryofmanufactureTest.php @@ -24,7 +24,6 @@ protected function setUp(): void public function testGetAllOptions() { - CacheCleaner::cleanAll(); $allOptions = $this->model->getAllOptions(); $cachedAllOptions = $this->model->getAllOptions(); $this->assertEquals($allOptions, $cachedAllOptions); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 481ec6aeac0f2..d20bf2907c780 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -593,6 +593,102 @@ public function updateImageDataProvider(): array ]; } + /** + * Tests that images are added correctly + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @dataProvider addImagesDataProvider + * @param string $addFromStore + * @param array $newImages + * @param string $viewFromStore + * @param array $expectedImages + * @param array $select + * @return void + */ + public function testAddImages( + string $addFromStore, + array $newImages, + string $viewFromStore, + array $expectedImages, + array $select = ['file', 'label', 'position'] + ): void { + $storeId = (int)$this->storeRepository->get($addFromStore)->getId(); + $product = $this->getProduct($storeId); + $images = $product->getData('media_gallery')['images']; + $images = array_merge($images, $newImages); + $product->setData('media_gallery', ['images' => $images]); + $this->updateHandler->execute($product); + $storeId = (int)$this->storeRepository->get($viewFromStore)->getId(); + $product = $this->getProduct($storeId); + $actualImages = array_map( + function (\Magento\Framework\DataObject $item) use ($select) { + return $item->toArray($select); + }, + $product->getMediaGalleryImages()->getItems() + ); + $this->assertEquals($expectedImages, array_values($actualImages)); + } + + /** + * @return array[] + */ + public function addImagesDataProvider(): array + { + return [ + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'default', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'file' => '/m/a/magento_small_image.jpg', + 'label' => null, + 'position' => 2, + ], + ] + ], + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'file' => '/m/a/magento_small_image.jpg', + 'label' => 'New Image Alt Text', + 'position' => 2, + ], + ] + ] + ]; + } + /** * Check product image link and product image exist * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php index 5b9c7b267f188..7d9e9a48093cb 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php @@ -52,7 +52,6 @@ class AttributeTest extends \PHPUnit\Framework\TestCase */ protected function setUp(): void { - CacheCleaner::cleanAll(); $this->objectManager = Bootstrap::getObjectManager(); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $this->attributeRepository = $this->objectManager->get(AttributeRepository::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php index c57e981f772de..66e117a61ed2c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php @@ -10,14 +10,21 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\UrlInterface; +use Magento\Indexer\Cron\UpdateMview; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -26,6 +33,7 @@ * Tests category resource model * * @see \Magento\Catalog\Model\ResourceModel\Category + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategoryTest extends TestCase { @@ -54,6 +62,11 @@ class CategoryTest extends TestCase /** @var WriteInterface */ private $mediaDirectory; + /** + * @var ProductResource + */ + private $productResource; + /** * @inheritdoc */ @@ -68,6 +81,7 @@ protected function setUp(): void $this->categoryCollection = $this->objectManager->get(CategoryCollectionFactory::class)->create(); $this->filesystem = $this->objectManager->get(Filesystem::class); $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->productResource = Bootstrap::getObjectManager()->get(ProductResource::class); } /** @@ -116,6 +130,128 @@ public function testAddImageForCategory(): void $this->assertFileExists($this->mediaDirectory->getAbsolutePath($imageRelativePath)); } + /** + * Test that adding or removing products in a category should not trigger full reindex in scheduled update mode + * + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Catalog/_files/catalog_category_product_reindex_all.php + * @magentoDataFixture Magento/Catalog/_files/catalog_product_category_reindex_all.php + * @magentoDataFixture Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php + * @dataProvider catalogProductChangesWithScheduledUpdateDataProvider + * @param array $products + * @return void + */ + public function testCatalogProductChangesWithScheduledUpdate(array $products): void + { + // products are ordered by entity_id DESC because their positions are same and equal to 0 + $initialProducts = ['simple1002', 'simple1001', 'simple1000']; + $defaultStoreId = (int) $this->storeManager->getDefaultStoreView()->getId(); + $category = $this->getCategory(['name' => 'Category 999']); + $expectedProducts = array_keys($products); + $productIdsBySkus = $this->productResource->getProductsIdsBySkus($expectedProducts); + $postedProducts = []; + foreach ($products as $sku => $position) { + $postedProducts[$productIdsBySkus[$sku]] = $position; + } + $category->setPostedProducts($postedProducts); + $this->categoryResource->save($category); + // Indices should not be invalidated when adding/removing/reordering products in a category. + $categoryProductIndexer = $this->getIndexer(CategoryProductIndexer::INDEXER_ID); + $this->assertTrue( + $categoryProductIndexer->isValid(), + '"Indexed category/products association" indexer should not be invalidated.' + ); + $productCategoryIndexer = $this->getIndexer(ProductCategoryIndexer::INDEXER_ID); + $this->assertTrue( + $productCategoryIndexer->isValid(), + '"Indexed product/categories association" indexer should not be invalidated.' + ); + // catalog products is not update until partial reindex occurs + $collection = $this->getCategoryProducts($category, $defaultStoreId); + $this->assertEquals($initialProducts, $collection->getColumnValues('sku')); + // Execute MVIEW cron handler for cron job "indexer_update_all_views" + /** @var $mViewCron UpdateMview */ + $mViewCron = $this->objectManager->create(UpdateMview::class); + $mViewCron->execute(); + $collection = $this->getCategoryProducts($category, $defaultStoreId); + $this->assertEquals($expectedProducts, $collection->getColumnValues('sku')); + } + + /** + * @return array + */ + public function catalogProductChangesWithScheduledUpdateDataProvider(): array + { + return [ + 'change products position' => [ + [ + 'simple1002' => 1, + 'simple1000' => 2, + 'simple1001' => 3, + ] + ], + 'Add new product' => [ + [ + 'simple1002' => 1, + 'simple1000' => 2, + 'simple-1' => 3, + 'simple1001' => 4, + ] + ], + 'Delete product' => [ + [ + 'simple1002' => 1, + 'simple1000' => 2, + ] + ] + ]; + } + + /** + * @param CategoryModel $category + * @param int $defaultStoreId + * @return ProductCollection + */ + private function getCategoryProducts(CategoryModel $category, int $defaultStoreId) + { + /** @var ProductCollection $collection */ + $collection = $this->objectManager->create(ProductCollection::class); + $collection->setStoreId($defaultStoreId); + $collection->addCategoryFilter($category); + $collection->addAttributeToSort('position'); + return $collection; + } + + /** + * @param array $filters + * @return CategoryModel + */ + private function getCategory(array $filters): CategoryModel + { + /** @var CategoryCollection $categoryCollection */ + $categoryCollection = $this->objectManager->create(CategoryCollection::class); + foreach ($filters as $field => $value) { + $categoryCollection->addFieldToFilter($field, $value); + } + + return $categoryCollection->getFirstItem(); + } + + /** + * @param string $indexerId + * @return IndexerInterface + */ + private function getIndexer(string $indexerId): IndexerInterface + { + /** @var IndexerRegistry $indexerRegistry */ + $indexerRegistry = $this->objectManager->get(IndexerRegistry::class); + return $indexerRegistry->get($indexerId); + } + /** * Prepare image url for image data * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index 8ad346af068b4..d5e7d94ec0cae 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -39,7 +39,6 @@ public function testModifyMeta() { $inputMeta = include __DIR__ . '/_files/input_meta_for_categories.php'; $expectedCategories = include __DIR__ . '/_files/expected_categories.php'; - CacheCleaner::cleanAll(); $this->assertCategoriesInMeta($expectedCategories, $this->object->modifyMeta($inputMeta)); // Verify cached data $this->assertCategoriesInMeta($expectedCategories, $this->object->modifyMeta($inputMeta)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_product_reindex_all.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_product_reindex_all.php new file mode 100644 index 0000000000000..3dfba9266cddc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_product_reindex_all.php @@ -0,0 +1,16 @@ +get(IndexerRegistry::class); + +$model = $indexRegistry->get(CategoryProductIndexer::INDEXER_ID); +$model->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_product_category_reindex_all.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_product_category_reindex_all.php new file mode 100644 index 0000000000000..6143933ba3d6c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_product_category_reindex_all.php @@ -0,0 +1,16 @@ +get(IndexerRegistry::class); + +$model = $indexRegistry->get(ProductCategoryIndexer::INDEXER_ID); +$model->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_catalog_product_reindex_schedule_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_catalog_product_reindex_schedule_rollback.php index 429f89abb6ae7..8909b258b9f9c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_catalog_product_reindex_schedule_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_catalog_product_reindex_schedule_rollback.php @@ -5,8 +5,14 @@ */ declare(strict_types=1); -use Magento\Catalog\Model\Indexer\Product\Price\Processor; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\TestFramework\Helper\Bootstrap; -$indexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); -$indexerProcessor->getIndexer()->setScheduled(false); +/** @var IndexerRegistry $indexRegistry */ +$indexRegistry = Bootstrap::getObjectManager()->get(IndexerRegistry::class); + +$model = $indexRegistry->get('catalog_category_product'); +$model->setScheduled(false); + +$model = $indexRegistry->get('catalog_product_category'); +$model->setScheduled(false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes.php new file mode 100644 index 0000000000000..8764e12916d8b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes.php @@ -0,0 +1,165 @@ +get(Config::class); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); + +for ($i = 1; $i <= 2; $i++) { + $attributeModel = $attributeFactory->create(); + $attributeModel->setData( + [ + 'attribute_code' => 'test_attribute_' . $i, + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Attribute ' . $i], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'], + 'position' => 3 - $i + ] + ); + $attribute = $attributeRepository->save($attributeModel); + $installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); +} + +CacheCleaner::cleanAll(); +$eavConfig->clear(); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setName('Simple Product1') + ->setSku('simple1') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(SourceType::TYPE_IN_CART) + ->setPrice(10) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('5.99'); +$simple1 = $productRepository->save($product); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setName('Simple Product2') + ->setSku('simple2') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(SourceType::TYPE_ON_GESTURE) + ->setPrice(20) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('15.99'); +$simple2 = $productRepository->save($product); + +/** @var CategoryInterfaceFactory $categoryInterfaceFactory */ +$categoryInterfaceFactory = $objectManager->get(CategoryInterfaceFactory::class); + +$category = $categoryInterfaceFactory->create(); +$category->isObjectNew(true); +$category->setId(3334) + ->setCreatedAt('2014-06-23 09:50:07') + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2/333') + ->setLevel(2) + ->setAvailableSortBy(['position', 'name']) + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setPostedProducts( + [ + $simple1->getId() => 10, + $simple2->getId() => 11 + ] + ); +$category->save(); + +/** @var Collection $indexerCollection */ +$indexerCollection = $objectManager->get(Collection::class); +$indexerCollection->load(); +/** @var Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes_rollback.php new file mode 100644 index 0000000000000..49e2b549552e6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes_rollback.php @@ -0,0 +1,63 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +foreach (['simple1', 'simple2'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (NoSuchEntityException $exception) { + //Product already removed + } +} + +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var GetCategoryByName $getCategoryByName */ +$getCategoryByName = $objectManager->get(GetCategoryByName::class); +$category = $getCategoryByName->execute('Category 1'); +try { + if ($category->getId()) { + $categoryRepository->delete($category); + } +} catch (NoSuchEntityException $exception) { + //Category already removed +} + +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + +try { + for ($i = 1; $i <= 2; $i++) { + $attribute = $attributeRepository->get('test_attribute_' . $i); + $attributeRepository->delete($attribute); + } +} catch (NoSuchEntityException $exception) { + //Attribute already removed +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 01a6bfe7b39b6..3ca6754c77767 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -3330,4 +3330,89 @@ public function testUpdateImageByNameNotPrefixedWithSlash() $imageItems = $product->getMediaGalleryImages()->getItems(); $this->assertCount(0, $imageItems); } + + /** + * Tests that images are imported correctly + * + * @magentoDataFixture mediaImportImageFixture + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @dataProvider importImagesDataProvider + * @magentoAppIsolation enabled + * @param string $importFile + * @param string $productSku + * @param string $storeCode + * @param array $expectedImages + * @param array $select + */ + public function testImportImages( + string $importFile, + string $productSku, + string $storeCode, + array $expectedImages, + array $select = ['file', 'label', 'position'] + ): void { + $this->importDataForMediaTest($importFile); + $product = $this->getProductBySku($productSku, $storeCode); + $actualImages = array_map( + function (\Magento\Framework\DataObject $item) use ($select) { + return $item->toArray($select); + }, + $product->getMediaGalleryImages()->getItems() + ); + $this->assertEquals($expectedImages, array_values($actualImages)); + } + + /** + * @return array[] + */ + public function importImagesDataProvider(): array + { + return [ + [ + 'import_media_additional_images_storeview.csv', + 'simple', + 'default', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1 + ], + [ + 'file' => '/m/a/magento_additional_image_one.jpg', + 'label' => null, + 'position' => 2 + ], + [ + 'file' => '/m/a/magento_additional_image_two.jpg', + 'label' => null, + 'position' => 3 + ], + ] + ], + [ + 'import_media_additional_images_storeview.csv', + 'simple', + 'fixturestore', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1 + ], + [ + 'file' => '/m/a/magento_additional_image_one.jpg', + 'label' => 'Additional Image Label One', + 'position' => 2 + ], + [ + 'file' => '/m/a/magento_additional_image_two.jpg', + 'label' => 'Additional Image Label Two', + 'position' => 3 + ], + ] + ] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images_storeview.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images_storeview.csv new file mode 100644 index 0000000000000..ed8755a73fcb1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images_storeview.csv @@ -0,0 +1,2 @@ +"sku","store_view_code","additional_images","additional_image_labels" +"simple","fixturestore","magento_additional_image_one.jpg, magento_additional_image_two.jpg","Additional Image Label One,Additional Image Label Two" diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php index 0432649455abe..e3e3d3e3972e5 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php @@ -85,6 +85,7 @@ public function productDataProvider(): array 'sku' => 'test-product', 'name' => 'test product', 'price' => 150, + 'website_ids' => [1] ], 'expected_data' => [ [ @@ -104,6 +105,7 @@ public function productDataProvider(): array 'name' => 'test product', 'price' => 150, 'url_key' => 'test-product-url-key', + 'website_ids' => [1] ], 'expected_data' => [ [ @@ -123,6 +125,7 @@ public function productDataProvider(): array 'name' => 'test product', 'price' => 150, 'url_key' => 'test-product-url-key', + 'website_ids' => [1] ], 'expected_data' => [], ], @@ -201,6 +204,7 @@ public function existingUrlKeyProvider(): array 'name' => 'test-simple-product', 'price' => 150, 'url_key' => 'simple-product', + 'store_ids' => [1] ], 'with_autogenerated_existing_product_url_key' => [ 'type_id' => Type::TYPE_SIMPLE, @@ -208,6 +212,7 @@ public function existingUrlKeyProvider(): array 'sku' => 'test-simple-product', 'name' => 'simple product', 'price' => 150, + 'store_ids' => [1] ], 'with_specified_existing_category_url_key' => [ 'type_id' => Type::TYPE_SIMPLE, @@ -216,6 +221,7 @@ public function existingUrlKeyProvider(): array 'name' => 'test-simple-product', 'price' => 150, 'url_key' => 'category-1', + 'store_ids' => [1] ], 'with_autogenerated_existing_category_url_key' => [ 'type_id' => Type::TYPE_SIMPLE, @@ -223,6 +229,7 @@ public function existingUrlKeyProvider(): array 'sku' => 'test-simple-product', 'name' => 'category 1', 'price' => 150, + 'store_ids' => [1] ], ], ]; diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php index c3efd660792c0..82631220730de 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php @@ -5,25 +5,46 @@ */ namespace Magento\CatalogUrlRewrite\Observer; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use PHPUnit\Framework\TestCase; /** * @magentoAppArea adminhtml * @magentoDbIsolation disabled */ -class ProductProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\TestCase +class ProductProcessUrlRewriteSavingObserverTest extends TestCase { - /** @var \Magento\Framework\ObjectManagerInterface */ - protected $objectManager; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; /** * Set up */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); } /** @@ -32,8 +53,8 @@ protected function setUp(): void */ private function getActualResults(array $filter) { - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); $actualResults = []; foreach ($urlFinder->findAllByData($filter) as $url) { $actualResults[] = [ @@ -53,16 +74,14 @@ private function getActualResults(array $filter) */ public function testUrlKeyHasChangedInGlobalContext() { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository*/ - $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $product*/ - $product = $productRepository->get('product1'); + $testStore1 = $this->storeManager->getStore('default'); + $testStore4 = $this->storeManager->getStore('test'); - /** @var StoreManagerInterface $storeManager */ - $storeManager = $this->objectManager->get(StoreManagerInterface::class); - $storeManager->setCurrentStore(0); + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); - $testStore = $storeManager->getStore('test'); $productFilter = [ UrlRewrite::ENTITY_TYPE => 'product', ]; @@ -73,14 +92,14 @@ public function testUrlKeyHasChangedInGlobalContext() 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore4->getId(), ], ]; $actual = $this->getActualResults($productFilter); @@ -91,7 +110,7 @@ public function testUrlKeyHasChangedInGlobalContext() $product->setData('save_rewrites_history', true); $product->setUrlKey('new-url'); $product->setUrlPath('new-path'); - $product->save(); + $this->productRepository->save($product); $expected = [ [ @@ -99,28 +118,28 @@ public function testUrlKeyHasChangedInGlobalContext() 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], [ 'request_path' => "new-url.html", 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore4->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "new-url.html", 'is_auto_generated' => 0, 'redirect_type' => 301, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "new-url.html", 'is_auto_generated' => 0, 'redirect_type' => 301, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore4->getId(), ], ]; @@ -136,16 +155,13 @@ public function testUrlKeyHasChangedInGlobalContext() */ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository*/ - $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $product*/ - $product = $productRepository->get('product1'); + $testStore1 = $this->storeManager->getStore('default'); + $testStore4 = $this->storeManager->getStore('test'); - /** @var StoreManagerInterface $storeManager */ - $storeManager = $this->objectManager->get(StoreManagerInterface::class); - $storeManager->setCurrentStore(1); + $this->storeManager->setCurrentStore($testStore1); - $testStore = $storeManager->getStore('test'); + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); $productFilter = [ UrlRewrite::ENTITY_TYPE => 'product', @@ -154,7 +170,7 @@ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() $product->setData('save_rewrites_history', true); $product->setUrlKey('new-url'); $product->setUrlPath('new-path'); - $product->save(); + $this->productRepository->save($product); $expected = [ [ @@ -162,21 +178,21 @@ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore4->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "new-url.html", 'is_auto_generated' => 0, 'redirect_type' => 301, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], ]; @@ -192,16 +208,13 @@ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() */ public function testUrlKeyHasChangedInStoreviewContextWithoutPermanentRedirection() { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository*/ - $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $product*/ - $product = $productRepository->get('product1'); + $testStore1 = $this->storeManager->getStore('default'); + $testStore4 = $this->storeManager->getStore('test'); - /** @var StoreManagerInterface $storeManager */ - $storeManager = $this->objectManager->get(StoreManagerInterface::class); - $storeManager->setCurrentStore(1); + $this->storeManager->setCurrentStore(1); - $testStore = $storeManager->getStore('test'); + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); $productFilter = [ UrlRewrite::ENTITY_TYPE => 'product', @@ -210,7 +223,7 @@ public function testUrlKeyHasChangedInStoreviewContextWithoutPermanentRedirectio $product->setData('save_rewrites_history', false); $product->setUrlKey('new-url'); $product->setUrlPath('new-path'); - $product->save(); + $this->productRepository->save($product); $expected = [ [ @@ -218,17 +231,402 @@ public function testUrlKeyHasChangedInStoreviewContextWithoutPermanentRedirectio 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testAddAndRemoveProductFromWebsite() + { + $testStore1 = $this->storeManager->getStore('default'); + $testStore2 = $this->storeManager->getStore('fixture_second_store'); + $testStore3 = $this->storeManager->getStore('fixture_third_store'); + $testStore4 = $this->storeManager->getStore('test'); + + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); + + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + //Product in 1st website. Should result in being in 1st and 4th stores. + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Add product to websites corresponding to all 4 stores. + //Rewrites should be present for all stores. + $product->setWebsiteIds( + array_unique( + [ + $testStore1->getWebsiteId(), + $testStore2->getWebsiteId(), + $testStore3->getWebsiteId(), + $testStore4->getWebsiteId(), + ] + ) + ); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore3->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ] + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Remove product from stores 1 and 4 and leave assigned to stores 2 and 3. + $product->setWebsiteIds( + array_unique( + [ + $testStore2->getWebsiteId(), + $testStore3->getWebsiteId(), + ] + ) + ); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore3->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testChangeVisibilityGlobalScope() + { + $testStore1 = $this->storeManager->getStore('default'); + $testStore2 = $this->storeManager->getStore('fixture_second_store'); + $testStore3 = $this->storeManager->getStore('fixture_third_store'); + $testStore4 = $this->storeManager->getStore('test'); + + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); + + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + //Product in 1st website. Should result in being in 1st and 4th stores. + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ] + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Set product to be not visible at global scope + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); + $this->productRepository->save($product); + $this->assertEmpty($this->getActualResults($productFilter)); + + //Add product to websites corresponding to all 4 stores. + //Rewrites should not be present as the product is hidden + //at the global scope. + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product->setWebsiteIds( + array_unique( + [ + $testStore1->getWebsiteId(), + $testStore2->getWebsiteId(), + $testStore3->getWebsiteId(), + $testStore4->getWebsiteId(), + ] + ) + ); + $this->productRepository->save($product); + $expected = []; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Set product to be visible at global scope + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product->setVisibility(Visibility::VISIBILITY_BOTH); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore3->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testChangeVisibilityLocalScope() + { + $testStore1 = $this->storeManager->getStore('default'); + $testStore2 = $this->storeManager->getStore('fixture_second_store'); + $testStore3 = $this->storeManager->getStore('fixture_third_store'); + $testStore4 = $this->storeManager->getStore('test'); + + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); + + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + //Product in 1st website. Should result in being in 1st and 4th stores. + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Set product to be not visible at store 4 scope + //Rewrite should only be present for store 1 + $this->storeManager->setCurrentStore($testStore4); + $product = $this->productRepository->get('product1'); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + self::assertCount(count($expected), $actual); + + //Add product to websites corresponding to all 4 stores. + //Rewrites should be present for stores 1,2 and 3. + //No rewrites should be present for store 4 as that is not visible + //at local scope. + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product = $this->productRepository->get('product1'); + $product->getExtensionAttributes()->setWebsiteIds( + array_unique( + [ + $testStore1->getWebsiteId(), + $testStore2->getWebsiteId(), + $testStore3->getWebsiteId(), + $testStore4->getWebsiteId() + ], + ) + ); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore3->getId(), ], ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + //Set product to be visible at store 4 scope only + $this->storeManager->setCurrentStore($testStore4); + $product = $this->productRepository->get('product1'); + $product->setVisibility(Visibility::VISIBILITY_BOTH); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore3->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; $actual = $this->getActualResults($productFilter); foreach ($expected as $row) { $this->assertContainsEquals($row, $actual); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewritesTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewritesTest.php new file mode 100644 index 0000000000000..f958027f413e3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewritesTest.php @@ -0,0 +1,72 @@ +action = $objectManager->get(Action::class); + $this->storeWebsiteRelation = $objectManager->get(StoreWebsiteRelationInterface::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + */ + public function testUpdateUrlRewrites() + { + /** @var Website $website */ + $websiteRepository = Bootstrap::getObjectManager()->get(WebsiteRepository::class); + $website = $websiteRepository->get('test'); + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $product = $productRepository->get('simple1', false, null, true); + $this->action->updateWebsites([$product->getId()], [$website->getId()], 'add'); + $storeIds = $this->storeWebsiteRelation->getStoreByWebsiteId($website->getId()); + + $this->assertStringContainsString( + $product->getUrlKey() . '.html', + $product->setStoreId(reset($storeIds))->getProductUrl() + ); + + $this->action->updateWebsites([$product->getId()], [$website->getId()], 'remove'); + $product->setRequestPath(''); + $url = $product->setStoreId(reset($storeIds))->getProductUrl(); + $this->assertStringNotContainsString( + $product->getUrlKey() . '.html', + $url + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php index 4cb4b00d08a84..e54ce16051d60 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php @@ -12,6 +12,8 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -120,6 +122,51 @@ public function testDifferentAddresses(bool $swapShipping): void $this->management->saveAddressInformation($idMask->getMaskedId(), $shippingInformation); } + /** + * Test save address information with customer custom address attribute for quote + * + * @return void + * + * @throws LocalizedException + * @throws NoSuchEntityException + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/Customer/_files/customer_address_with_custom_text_attribute.php + */ + public function testSaveAddressInformationWithCustomerCustomAddressAttribute(): void + { + $carts = $this->cartRepo->getList( + $this->searchCriteria->addFilter('reserved_order_id', 'test01')->create() + )->getItems(); + $currentQuote = array_pop($carts); + $guestCustomer = $this->customerRepo->get('JohnDoe@mail.com'); + + $customerCustomAddressAttribute = $guestCustomer->getCustomAttributes(); + + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $currentQuote->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $currentQuote->getBillingAddress(); + + if ($customerCustomAddressAttribute) { + $shippingAddress->setCustomAttributes($customerCustomAddressAttribute); + $billingAddress->setCustomAttributes($customerCustomAddressAttribute); + } + + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + $shippingInformation->setShippingCarrierCode('flatrate'); + /** @var QuoteIdMask $idMask */ + $idMask = $this->maskFactory->create(); + $idMask->load($currentQuote->getId(), 'quote_id'); + + $paymentDetails = $this->management->saveAddressInformation($idMask->getMaskedId(), $shippingInformation); + $this->assertNotEmpty($paymentDetails); + $this->assertEquals($currentQuote->getGrandTotal(), $paymentDetails->getTotals()->getSubtotal()); + } + /** * Different variations for addresses test. * diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php index f0ba56f7179aa..4c003c209e3c2 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php @@ -67,7 +67,7 @@ $iDate++; break; case ProductCustomOptionInterface::OPTION_GROUP_FILE: - $value = 'test.jpg'; + $value = null; break; default: $value = 'test'; diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AddressMetadataTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AddressMetadataTest.php index 647c386e2b784..4443d170e388a 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AddressMetadataTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AddressMetadataTest.php @@ -19,7 +19,6 @@ class AddressMetadataTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - CacheCleaner::cleanAll(); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $objectManager->configure( [ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php index c3e08b5294679..63d7019ee4f61 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php @@ -29,7 +29,6 @@ class CustomerMetadataTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - CacheCleaner::cleanAll(); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $objectManager->configure( [\Magento\Framework\Api\ExtensionAttribute\Config\Reader::class => [ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Group/ResolverTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Group/ResolverTest.php new file mode 100644 index 0000000000000..0f85a94f639d1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Group/ResolverTest.php @@ -0,0 +1,26 @@ +create(Resolver::class); + $groupId = $resolver->resolve($customerId); + $this->assertEquals($groupId, $expectedGroupId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute.php new file mode 100644 index 0000000000000..ebe4ad76405ef --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute.php @@ -0,0 +1,96 @@ +create(Config::class) + ->getEntityType('customer'); + +/** @var $attributeSet Set */ +$attributeSet = Bootstrap::getObjectManager() + ->create(Set::class); + +$select = Bootstrap::getObjectManager()->create( + Attribute::class, + [ + 'data' => [ + 'frontend_input' => 'text', + 'frontend_label' => ['test_text_attribute'], + 'sort_order' => 1, + 'backend_type' => 'varchar', + 'is_user_defined' => 1, + 'is_system' => 0, + 'is_used_in_grid' => 1, + 'is_required' => '0', + 'is_visible' => 1, + 'used_in_forms' => [ + 'customer_address_edit', + 'adminhtml_customer_address' + ], + 'attribute_set_id' => $entityType->getDefaultAttributeSetId(), + 'attribute_group_id' => $attributeSet->getDefaultGroupId($entityType->getDefaultAttributeSetId()), + 'entity_type_id' => $entityType->getId(), + 'default_value' => '', + ], + ] +); +$select->setAttributeCode('test_text_attribute'); +$select->save(); + +$customer = $objectManager + ->create(Customer::class); +$customer->setWebsiteId(1) + ->setEntityId(1) + ->setEntityTypeId($entityType->getId()) + ->setAttributeSetId($entityType->getDefaultAttributeSetId()) + ->setEmail('JohnDoe@mail.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('John') + ->setLastname('Doe') + ->setGender(2) + ->setTestTextAttribute('123'); +$customer->isObjectNew(true); +// Create address +$address = $objectManager->create(Address::class); +// default_billing and default_shipping information would not be saved, it is needed only for simple check +$address->addData( + [ + 'firstname' => 'Charles', + 'lastname' => 'Alston', + 'street' => '3781 Neuport Lane', + 'city' => 'Panola', + 'country_id' => 'US', + 'region_id' => '51', + 'postcode' => '30058', + 'telephone' => '770-322-3514', + 'default_billing' => 1, + 'default_shipping' => 1, + ] +); +// Assign customer and address +$customer->addAddress($address); +$customer->save(); +// Mark last address as default billing and default shipping for current customer +$customer->setDefaultBilling($address->getId()); +$customer->setDefaultShipping($address->getId()); +$customer->save(); + +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customer'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customer', $customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute_rollback.php new file mode 100644 index 0000000000000..3b276b77fbed5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute_rollback.php @@ -0,0 +1,33 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $attribute Attribute */ +$attribute = Bootstrap::getObjectManager()->create( + Attribute::class +); +$attribute->loadByCode('customer', 'test_text_attribute'); +$attribute->delete(); + +/** @var Customer $customer */ +$customer = Bootstrap::getObjectManager() + ->create(Customer::class); +$customer->setWebsiteId(1); +$customer->loadByEmail('JohnDoe@mail.com'); +$customer->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php index f9a1d2923e5be..552040489e253 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -17,19 +17,22 @@ use Magento\Quote\Model\Quote\Address\RateRequest; use Magento\Quote\Model\Quote\Address\RateResult\Error; use Magento\Shipping\Model\Shipment\Request; +use Magento\Shipping\Model\Simplexml\Element as ShippingElement; use Magento\Shipping\Model\Tracking\Result\Status; use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\HTTP\AsyncClientInterfaceMock; -use Magento\Shipping\Model\Simplexml\Element as ShippingElement; +use PHPUnit\Framework\TestCase; /** * Test for DHL integration. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CarrierTest extends \PHPUnit\Framework\TestCase +class CarrierTest extends TestCase { + private const PRODUCT_NAME_SPECIAL_CHARS = 'Φυστίκι Ψημένο με Αλάτι Συσκευασία'; + /** * @var Carrier */ @@ -254,10 +257,16 @@ private function assertTrackingResult($expectedTrackingData, $trackingResults): * @param string $origCountryId * @param string $expectedRegionCode * @param string $destCountryId + * @param bool|null $isProductNameContainsSpecialChars + * @return void * @dataProvider requestToShipmentDataProvider */ - public function testRequestToShip(string $origCountryId, string $expectedRegionCode, string $destCountryId): void - { + public function testRequestToShip( + string $origCountryId, + string $expectedRegionCode, + string $destCountryId, + bool $isProductNameContainsSpecialChars = false + ): void { $this->config->setValue( 'shipping/origin/country_id', $origCountryId, @@ -274,6 +283,8 @@ public function testRequestToShip(string $origCountryId, string $expectedRegionC ) ] ); + $productName = $isProductNameContainsSpecialChars ? self::PRODUCT_NAME_SPECIAL_CHARS : 'item_name'; + //phpcs:enable Magento2.Functions.DiscouragedFunction $request = new Request( [ @@ -291,7 +302,7 @@ public function testRequestToShip(string $origCountryId, string $expectedRegionC ], 'items' => [ 'item1' => [ - 'name' => 'item_name', + 'name' => $productName, ], ], ], @@ -329,10 +340,15 @@ public function testRequestToShip(string $origCountryId, string $expectedRegionC $requestElement->Request->ServiceHeader->MessageReference = 'MAGE_SHIP_28TO32_Char_CHECKED'; $requestElement->Request->ServiceHeader->MessageTime = 'currentTime'; $requestElement->ShipmentDetails->Date = 'currentTime'; - $this->assertXmlStringEqualsXmlString( - $this->getExpectedLabelRequestXml($origCountryId, $destCountryId, $expectedRegionCode), - $requestElement->asXML() + + $expectedLabelRequest = $this->getExpectedLabelRequestXml( + $origCountryId, + $destCountryId, + $expectedRegionCode, + $isProductNameContainsSpecialChars ); + + $this->assertXmlStringEqualsXmlString($expectedLabelRequest, $requestElement->asXML()); } /** @@ -351,7 +367,10 @@ public function requestToShipmentDataProvider(): array ], [ 'DE', 'EU', 'DE' - ] + ], + [ + 'GB', 'EU', 'US', true + ], ]; } @@ -361,12 +380,14 @@ public function requestToShipmentDataProvider(): array * @param string $origCountryId * @param string $destCountryId * @param string $regionCode + * @param bool $isProductNameContainsSpecialChars * @return string */ private function getExpectedLabelRequestXml( string $origCountryId, string $destCountryId, - string $regionCode + string $regionCode, + bool $isProductNameContainsSpecialChars ): string { $countryNames = [ 'US' => 'United States Of America', @@ -387,6 +408,10 @@ private function getExpectedLabelRequestXml( $expectedRequestElement->Shipper->CountryName = $countryNames[$origCountryId]; $expectedRequestElement->RegionCode = $regionCode; + if ($isProductNameContainsSpecialChars) { + $expectedRequestElement->ShipmentDetails->Pieces->Piece->PieceContents = self::PRODUCT_NAME_SPECIAL_CHARS; + } + return $expectedRequestElement->asXML(); } diff --git a/dev/tests/integration/testsuite/Magento/Directory/Block/DataTest.php b/dev/tests/integration/testsuite/Magento/Directory/Block/DataTest.php index ea2368a35c2f2..ccc1ea3f2f0b9 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Block/DataTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Block/DataTest.php @@ -22,7 +22,6 @@ protected function setUp(): void public function testGetCountryHtmlSelect() { - CacheCleaner::cleanAll(); $result = $this->block->getCountryHtmlSelect(); $resultTwo = $this->block->getCountryHtmlSelect(); $this->assertEquals($result, $resultTwo); @@ -30,7 +29,6 @@ public function testGetCountryHtmlSelect() public function testGetRegionHtmlSelect() { - CacheCleaner::cleanAll(); $result = $this->block->getRegionHtmlSelect(); $resultTwo = $this->block->getRegionHtmlSelect(); $this->assertEquals($result, $resultTwo); diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php index a2865d52517fa..85bf40b342fb5 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php @@ -33,7 +33,6 @@ protected function setUp(): void public function testGetEntityAttributeCodes() { $entityType = 'test'; - CacheCleaner::cleanAll(); $entityAttributeCodes1 = $this->config->getEntityAttributeCodes($entityType); $this->assertEquals( [ @@ -60,7 +59,6 @@ public function testGetEntityAttributeCodesWithObject() $testEntityType = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Entity\Type::class) ->loadByCode('test'); $attributeSetId = $testEntityType->getDefaultAttributeSetId(); - CacheCleaner::cleanAll(); $object = new DataObject( [ 'attribute_set_id' => $attributeSetId, @@ -86,7 +84,6 @@ public function testGetEntityAttributeCodesWithObject() public function testGetAttributes() { $entityType = 'test'; - CacheCleaner::cleanAll(); $attributes1 = $this->config->getAttributes($entityType); $expectedAttributeCodes = [ 'attribute_for_search_1', @@ -111,7 +108,6 @@ public function testGetAttributes() public function testGetAttribute() { $entityType = 'test'; - CacheCleaner::cleanAll(); $attribute1 = $this->config->getAttribute($entityType, 'attribute_for_search_1'); $this->assertEquals('attribute_for_search_1', $attribute1->getAttributeCode()); $this->assertEquals('varchar', $attribute1->getBackendType()); @@ -153,8 +149,7 @@ public function testGetAttributeWithCacheUserDefinedAttribute() $config = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class); $updatedAttribute = $config->getAttribute($entityType, 'foo'); $this->assertEquals('foo', $updatedAttribute->getFrontendLabel()); - // Clean cache - CacheCleaner::cleanAll(); + CacheCleaner::clean(['eav']); $config = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class); // Check that attribute data has changed $updatedAttributeAfterCacheClean = $config->getAttribute($entityType, 'foo'); diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php index b27e81129e1c3..cb7d288deb75a 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php @@ -55,7 +55,6 @@ class DefaultFrontendTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - CacheCleaner::cleanAll(); $this->objectManager = Bootstrap::getObjectManager(); $this->defaultFrontend = $this->objectManager->get(DefaultFrontend::class); diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/AttributeLoaderTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/AttributeLoaderTest.php index 7a0c66e7e2db2..817c37bca528b 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/AttributeLoaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/AttributeLoaderTest.php @@ -32,7 +32,6 @@ class AttributeLoaderTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - CacheCleaner::cleanAll(); $this->objectManager = Bootstrap::getObjectManager(); $this->attributeLoader = $this->objectManager->get(AttributeLoader::class); $entityType = $this->objectManager->create(\Magento\Eav\Model\Entity\Type::class) diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template_new_user_notification.php b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template_new_user_notification.php new file mode 100644 index 0000000000000..d742eb1a01414 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template_new_user_notification.php @@ -0,0 +1,21 @@ +create(\Magento\Email\Model\Template::class); +$template->setOptions(['area' => 'test area', 'store' => 1]); +$templateText = '{{trans "New User Notification Custom Text %first_name, ' . + '%last_name" first_name=$user.firstname last_name=$user.lastname}}'; +$template->setData( + [ + 'template_text' => $templateText, + 'template_code' => 'New User Notification Custom Code', + 'template_type' => \Magento\Email\Model\Template::TYPE_TEXT, + 'orig_template_code' => 'admin_emails_new_user_notification_template' + ] +); +$template->save(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Config/InitialTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Config/InitialTest.php index 43203d38e308f..2b3ebc217ab7b 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Config/InitialTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Config/InitialTest.php @@ -24,7 +24,6 @@ protected function setUp(): void public function testGetMetadata() { - CacheCleaner::cleanAll(); $this->assertEquals( $this->objectManager->create(Config::class)->getMetadata(), $this->objectManager->create(Config::class)->getMetadata() @@ -37,7 +36,6 @@ public function testGetMetadata() */ public function testGetData($scope) { - CacheCleaner::cleanAll(); $this->assertEquals( $this->objectManager->create(Config::class)->getData($scope), $this->objectManager->create(Config::class)->getData($scope) diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigLoaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigLoaderTest.php index 1a2b8772c2dc5..ed4e15bd78a0c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigLoaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigLoaderTest.php @@ -24,7 +24,6 @@ protected function setUp(): void public function testLoad() { - CacheCleaner::cleanAll(); $data = $this->object->load('global'); $this->assertNotEmpty($data); $cachedData = $this->object->load('global'); diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Route/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Route/ConfigTest.php index ff37b7d847921..d0c8c6083caee 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Route/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Route/ConfigTest.php @@ -28,7 +28,6 @@ protected function setUp(): void */ public function testGetRouteFrontName($route, $scope) { - CacheCleaner::cleanAll(); $this->assertEquals( $this->objectManager->create(Config::class)->getRouteFrontName($route, $scope), $this->objectManager->create(Config::class)->getRouteFrontName($route, $scope) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php index 857c068efe188..b02ccdb21e687 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php @@ -17,7 +17,9 @@ class ConfigTest extends \PHPUnit\Framework\TestCase */ public function testGetTopics() { - $topics = $this->getConfigInstance(__DIR__ . '/_files/valid_communication.xml')->getTopics(); + $topics = $this->getConfigInstance( + [__DIR__ . '/_files/valid_communication.xml', __DIR__ . '/_files/valid_communication_extra.xml'] + )->getTopics(); $expectedParsedTopics = include __DIR__ . '/_files/valid_communication_expected.php'; $this->assertEquals($expectedParsedTopics, $topics); } @@ -29,9 +31,11 @@ public function testGetTopics() public function testGetTopicsNumeric() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Service method specified in the definition of topic "customerDeletedNumbers" is not av'); + $this->expectExceptionMessage( + 'Service method specified in the definition of topic "customerDeletedNumbers" is not av' + ); - $this->getConfigInstance(__DIR__ . '/_files/valid_communication_numeric.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/valid_communication_numeric.xml'])->getTopics(); } // @codingStandardsIgnoreStart @@ -58,7 +62,7 @@ public function testGetTopicsNumericInvalid() $this->expectException(\Magento\Framework\Exception\LocalizedException::class); $this->expectExceptionMessage('The XML in file "0" is invalid:'); - $this->getConfigInstance(__DIR__ . '/_files/invalid_communication_numeric.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/invalid_communication_numeric.xml'])->getTopics(); } /** @@ -66,7 +70,9 @@ public function testGetTopicsNumericInvalid() */ public function testGetTopic() { - $topics = $this->getConfigInstance(__DIR__ . '/_files/valid_communication.xml')->getTopic('customerCreated'); + $topics = $this->getConfigInstance( + [__DIR__ . '/_files/valid_communication.xml', __DIR__ . '/_files/valid_communication_extra.xml'] + )->getTopic('customerCreated'); $expectedParsedTopics = include __DIR__ . '/_files/valid_communication_expected.php'; $this->assertEquals($expectedParsedTopics['customerCreated'], $topics); } @@ -80,7 +86,7 @@ public function testGetTopicInvalidName() $this->expectException(\Magento\Framework\Exception\LocalizedException::class); $this->expectExceptionMessage('Topic "invalidTopic" is not configured.'); - $this->getConfigInstance(__DIR__ . '/_files/valid_communication.xml')->getTopic('invalidTopic'); + $this->getConfigInstance([__DIR__ . '/_files/valid_communication.xml'])->getTopic('invalidTopic'); } /** @@ -88,9 +94,11 @@ public function testGetTopicInvalidName() public function testGetTopicsExceptionMissingRequest() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Either "request" or "schema" attribute must be specified for topic "customerUpdated"'); + $this->expectExceptionMessage( + 'Either "request" or "schema" attribute must be specified for topic "customerUpdated"' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_missing_request.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_missing_request.xml'])->getTopics(); } /** @@ -100,7 +108,7 @@ public function testGetTopicsExceptionNotExistingServiceMethod() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Service method specified in the definition of topic "customerRetrieved" is not'); - $this->getConfigInstance(__DIR__ . '/_files/communication_not_existing_service_method.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_not_existing_service_method.xml'])->getTopics(); } /** @@ -110,7 +118,7 @@ public function testGetTopicsExceptionNotExistingService() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Service method specified in the definition of topic "customerRetrieved" is not'); - $this->getConfigInstance(__DIR__ . '/_files/communication_not_existing_service.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_not_existing_service.xml'])->getTopics(); } /** @@ -118,9 +126,11 @@ public function testGetTopicsExceptionNotExistingService() public function testGetTopicsExceptionNoAttributes() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Either "request" or "schema" attribute must be specified for topic "customerRetrieved"'); + $this->expectExceptionMessage( + 'Either "request" or "schema" attribute must be specified for topic "customerRetrieved"' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_no_attributes.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_no_attributes.xml'])->getTopics(); } /** @@ -128,9 +138,11 @@ public function testGetTopicsExceptionNoAttributes() public function testGetTopicsExceptionInvalidResponseSchema() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Response schema definition for topic "customerUpdated" should reference existing'); + $this->expectExceptionMessage( + 'Response schema definition for topic "customerUpdated" should reference existing' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_response_not_existing_service.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_response_not_existing_service.xml'])->getTopics(); } /** @@ -138,9 +150,11 @@ public function testGetTopicsExceptionInvalidResponseSchema() public function testGetTopicsExceptionInvalidRequestSchema() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Request schema definition for topic "customerUpdated" should reference existing'); + $this->expectExceptionMessage( + 'Request schema definition for topic "customerUpdated" should reference existing' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_request_not_existing_service.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_request_not_existing_service.xml'])->getTopics(); } /** @@ -148,9 +162,12 @@ public function testGetTopicsExceptionInvalidRequestSchema() public function testGetTopicsExceptionMultipleHandlersSynchronousMode() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic "customerDeleted" is configured for synchronous requests, that is why it must'); + $this->expectExceptionMessage( + 'Topic "customerDeleted" is configured for synchronous requests, that is why it must' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_multiple_handlers_synchronous_mode.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_multiple_handlers_synchronous_mode.xml']) + ->getTopics(); } /** @@ -158,9 +175,11 @@ public function testGetTopicsExceptionMultipleHandlersSynchronousMode() public function testGetTopicsExceptionInvalidHandler() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Service method specified in the definition of handler "customHandler" for topic "custo'); + $this->expectExceptionMessage( + 'Service method specified in the definition of handler "customHandler" for topic "custo' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_not_existing_handler_method.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_not_existing_handler_method.xml'])->getTopics(); } /** @@ -168,10 +187,12 @@ public function testGetTopicsExceptionInvalidHandler() public function testGetTopicsExceptionInvalidTopicNameInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name "customerAdded" and attribute "name" = "customerCreated" must be equal'); + $this->expectExceptionMessage( + 'Topic name "customerAdded" and attribute "name" = "customerCreated" must be equal' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_invalid_topic_name.php' )->getTopics(); } @@ -184,7 +205,7 @@ public function testGetTopicsExceptionTopicWithoutDataInEnv() $this->expectExceptionMessage('Topic "customerCreated" must contain data'); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_topic_without_data.php' )->getTopics(); } @@ -197,7 +218,7 @@ public function testGetTopicsExceptionTopicWithMissedKeysInEnv() $this->expectExceptionMessage('Topic "customerCreated" has missed keys: [response]'); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_topic_with_missed_keys.php' )->getTopics(); } @@ -210,7 +231,7 @@ public function testGetTopicsExceptionTopicWithExcessiveKeysInEnv() $this->expectExceptionMessage('Topic "customerCreated" has excessive keys: [some_incorrect_key]'); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_topic_with_excessive_keys.php' )->getTopics(); } @@ -220,10 +241,12 @@ public function testGetTopicsExceptionTopicWithExcessiveKeysInEnv() public function testGetTopicsExceptionTopicWithNonMatchedNameInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name "customerDeleted" and attribute "name" = "customerRemoved" must be equal'); + $this->expectExceptionMessage( + 'Topic name "customerDeleted" and attribute "name" = "customerRemoved" must be equal' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_with_non_matched_name.php' )->getTopics(); } @@ -233,10 +256,12 @@ public function testGetTopicsExceptionTopicWithNonMatchedNameInEnv() public function testGetTopicsExceptionMultipleHandlersSynchronousModeInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic "customerDeleted" is configured for synchronous requests, that is why it must'); + $this->expectExceptionMessage( + 'Topic "customerDeleted" is configured for synchronous requests, that is why it must' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_multiple_handlers_synchronous_mode.php' )->getTopics(); } @@ -246,10 +271,12 @@ public function testGetTopicsExceptionMultipleHandlersSynchronousModeInEnv() public function testGetTopicsExceptionInvalidRequestSchemaInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Request schema definition for topic "customerCreated" should reference existing service'); + $this->expectExceptionMessage( + 'Request schema definition for topic "customerCreated" should reference existing service' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_request_not_existing_service.php' )->getTopics(); } @@ -259,10 +286,12 @@ public function testGetTopicsExceptionInvalidRequestSchemaInEnv() public function testGetTopicsExceptionInvalidResponseSchemaInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Response schema definition for topic "customerCreated" should reference existing type o'); + $this->expectExceptionMessage( + 'Response schema definition for topic "customerCreated" should reference existing type o' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_response_not_existing_service.php' )->getTopics(); } @@ -272,10 +301,12 @@ public function testGetTopicsExceptionInvalidResponseSchemaInEnv() public function testGetTopicsExceptionInvalidMethodInHandlerInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Service method specified in the definition of handler "customerCreatedFirst" for topic'); + $this->expectExceptionMessage( + 'Service method specified in the definition of handler "customerCreatedFirst" for topic' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_not_existing_handler_method.php' )->getTopics(); } @@ -285,10 +316,12 @@ public function testGetTopicsExceptionInvalidMethodInHandlerInEnv() public function testGetTopicsExceptionWithDisabledHandlerInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Disabled handler "default" for topic "customerCreated" cannot be added to the config fi'); + $this->expectExceptionMessage( + 'Disabled handler "default" for topic "customerCreated" cannot be added to the config fi' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_with_disabled_handler.php' )->getTopics(); } @@ -298,10 +331,12 @@ public function testGetTopicsExceptionWithDisabledHandlerInEnv() public function testGetTopicsExceptionIncorrectRequestSchemaTypeInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Request schema type for topic "customerCreated" must be "object_interface" or "service_'); + $this->expectExceptionMessage( + 'Request schema type for topic "customerCreated" must be "object_interface" or "service_' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_incorrect_request_schema_type.php' )->getTopics(); } @@ -311,10 +346,12 @@ public function testGetTopicsExceptionIncorrectRequestSchemaTypeInEnv() public function testGetTopicsExceptionIsNotBooleanTypeOfIsSynchronousInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The attribute "is_synchronous" for topic "customerCreated" should have the value of the'); + $this->expectExceptionMessage( + 'The attribute "is_synchronous" for topic "customerCreated" should have the value of the' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_is_synchronous_is_not_boolean.php' )->getTopics(); } @@ -322,16 +359,20 @@ public function testGetTopicsExceptionIsNotBooleanTypeOfIsSynchronousInEnv() /** * Create config instance initialized with configuration from $configFilePath * - * @param string $configFilePath + * @param array $configFilePaths * @param string|null $envConfigFilePath * @return \Magento\Framework\Communication\ConfigInterface */ - protected function getConfigInstance($configFilePath, $envConfigFilePath = null) + protected function getConfigInstance($configFilePaths, $envConfigFilePath = null) { $fileResolver = $this->getMockForAbstractClass(\Magento\Framework\Config\FileResolverInterface::class); + $fileResolverResult = []; + foreach ($configFilePaths as $configFilePath) { + $fileResolverResult[] = file_get_contents($configFilePath); + } $fileResolver->expects($this->any()) ->method('get') - ->willReturn([file_get_contents($configFilePath)]); + ->willReturn($fileResolverResult); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $xmlReader = $objectManager->create( \Magento\Framework\Communication\Config\Reader\XmlReader::class, diff --git a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_expected.php b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_expected.php index e384e779de74d..447197e10808e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_expected.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_expected.php @@ -33,6 +33,10 @@ 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, 'method' => 'delete', ], + 'customerCreatedExtra' => [ + 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, + 'method' => 'save', + ], 'saveNameNotDisabled' => [ 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, 'method' => 'save', diff --git a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_extra.xml b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_extra.xml new file mode 100644 index 0000000000000..fad468a5d288a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_extra.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_input.php b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_input.php index e099d5d45c877..082984ff15aaf 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_input.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_input.php @@ -35,6 +35,10 @@ 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, 'method' => 'delete', ], + 'customerCreatedExtra' => [ + 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, + 'method' => 'save', + ], 'saveNameNotDisabled' => [ 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, 'method' => 'save', diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php index 6e3391bd8959f..a93e8c198336e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php @@ -27,7 +27,6 @@ protected function setUp(): void set_error_handler(null); $this->resourceConnection = Bootstrap::getObjectManager() ->get(ResourceConnection::class); - CacheCleaner::cleanAll(); } protected function tearDown(): void diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php index ba2225fbe5eac..b77807a11da9b 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Mview\View; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Mview\View; /** * Test Class for \Magento\Framework\Mview\View\Changelog @@ -123,6 +124,54 @@ public function testClear() $this->assertEquals(1, $this->model->getVersion()); //the same that a table is empty } + /** + * Create entity table for MView + * + * @param string $tableName + * @return void + */ + private function createEntityTable(string $tableName) + { + $table = $this->resource->getConnection()->newTable( + $tableName + )->addColumn( + 'entity_id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], + 'Version ID' + )->addColumn( + 'additional_column', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['unsigned' => true, 'nullable' => false, 'default' => '0'], + 'Entity ID' + ); + $this->resource->getConnection()->createTable($table); + } + + public function testAdditionalColumns() + { + $tableName = 'test_mview_table'; + $this->createEntityTable($tableName); + $view = $this->objectManager->create(View::class); + $view->load('test_view_with_additional_columns'); + $view->subscribe(); + $this->connection->insert($tableName, ['entity_id' => 12, 'additional_column' => 13]); + $select = $this->connection->select() + ->from($view->getChangelog()->getName(), ['entity_id', 'test_additional_column']); + $actual = $this->connection->fetchAll($select); + $this->assertEquals( + [ + 'entity_id' => "12", + 'test_additional_column' => "13" + ], + reset($actual) + ); + $this->connection->dropTable($tableName); + $this->connection->dropTable($view->getChangelog()->getName()); + } + /** * Test for getList() method * diff --git a/dev/tests/integration/testsuite/Magento/Framework/Reflection/MethodsMapTest.php b/dev/tests/integration/testsuite/Magento/Framework/Reflection/MethodsMapTest.php index 585933422edb8..cbe3c552544d4 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Reflection/MethodsMapTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Reflection/MethodsMapTest.php @@ -24,7 +24,6 @@ protected function setUp(): void public function testGetMethodsMap() { - CacheCleaner::cleanAll(); $data = $this->object->getMethodsMap(\Magento\Framework\Reflection\MethodsMap::class); $this->assertArrayHasKey('getMethodsMap', $data); $cachedData = $this->object->getMethodsMap(\Magento\Framework\Reflection\MethodsMap::class); @@ -33,7 +32,6 @@ public function testGetMethodsMap() public function testGetMethodParams() { - CacheCleaner::cleanAll(); $data = $this->object->getMethodParams( \Magento\Framework\Reflection\MethodsMap::class, 'getMethodParams' diff --git a/dev/tests/integration/testsuite/Magento/Framework/TranslateTest.php b/dev/tests/integration/testsuite/Magento/Framework/TranslateTest.php index 5b48a9169c577..b216831bde33e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/TranslateTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/TranslateTest.php @@ -94,7 +94,6 @@ protected function setUp(): void public function testLoadData() { $data = $this->translate->loadData(null, true)->getData(); - CacheCleaner::cleanAll(); $this->translate->loadData()->getData(); $dataCached = $this->translate->loadData()->getData(); $this->assertEquals($data, $dataCached); diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Element/UiComponent/Config/Provider/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Element/UiComponent/Config/Provider/TemplateTest.php index f58882ee51493..92a51b85c8c4b 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/Element/UiComponent/Config/Provider/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Element/UiComponent/Config/Provider/TemplateTest.php @@ -49,7 +49,6 @@ public function testGetTemplate() \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); $this->objectManager->get(\Magento\Framework\View\DesignInterface::class) ->setDesignTheme('FrameworkViewUiComponent/default'); - CacheCleaner::cleanAll(); $resultOne = $this->model->getTemplate('test.xml'); $resultTwo = $this->model->getTemplate('test.xml'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_gift_options.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_gift_options.php new file mode 100644 index 0000000000000..c870fa53c5e39 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_gift_options.php @@ -0,0 +1,33 @@ +get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = $objectManager->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = $objectManager->get(CartRepositoryInterface::class); + + +/** @var \Magento\GiftMessage\Model\Message $message */ +$message = $objectManager->create(\Magento\GiftMessage\Model\Message::class); +$message->setSender('Romeo'); +$message->setRecipient('Mercutio'); +$message->setMessage('I thought all for the best.'); +$message->save(); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->setGiftMessageId($message->getId()); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Block/Checkout/OverviewTest.php b/dev/tests/integration/testsuite/Magento/Multishipping/Block/Checkout/OverviewTest.php index 5009f210404d0..182505cba4a61 100644 --- a/dev/tests/integration/testsuite/Magento/Multishipping/Block/Checkout/OverviewTest.php +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Block/Checkout/OverviewTest.php @@ -6,28 +6,58 @@ namespace Magento\Multishipping\Block\Checkout; +use Magento\Catalog\Model\ProductRepository; +use Magento\Checkout\Block\Cart\Item\Renderer; +use Magento\Framework\App\Area; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\RendererList; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + /** - * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * Verify default items template */ -class OverviewTest extends \PHPUnit\Framework\TestCase +class OverviewTest extends TestCase { /** - * @var \Magento\Multishipping\Block\Checkout\Overview + * @var Overview + */ + private $block; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Quote + */ + private $quote; + + /** + * @var ProductRepository */ - protected $_block; + private $product; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var Item */ - protected $_objectManager; + private $item; + /** + * @inheritdoc + */ protected function setUp(): void { - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_block = $this->_objectManager->get(\Magento\Framework\View\LayoutInterface::class) + Bootstrap::getInstance()->loadArea(Area::AREA_FRONTEND); + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class) ->createBlock( - \Magento\Multishipping\Block\Checkout\Overview::class, + Overview::class, 'checkout_overview', [ 'data' => [ @@ -37,33 +67,49 @@ protected function setUp(): void ] ); - $this->_block->addChild('renderer.list', \Magento\Framework\View\Element\RendererList::class); - $this->_block->getChildBlock( + $this->block->addChild('renderer.list', RendererList::class); + $this->block->getChildBlock( 'renderer.list' )->addChild( 'default', - \Magento\Checkout\Block\Cart\Item\Renderer::class, + Renderer::class, ['template' => 'cart/item/default.phtml'] ); + $this->quote = $this->objectManager->create(Quote::class); + $this->product = $this->objectManager->create(ProductRepository::class); + $this->item = $this->objectManager->create(Item::class); } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ public function testGetRowItemHtml() { - /** @var $item \Magento\Quote\Model\Quote\Item */ - $item = $this->_objectManager->create(\Magento\Quote\Model\Quote\Item::class); - /** @var $product \Magento\Catalog\Model\Product */ - $product = $this->_objectManager->create(\Magento\Catalog\Model\Product::class); - $product->load(1); - $item->setProduct($product); - /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->_objectManager->create(\Magento\Quote\Model\Quote::class); - $item->setQuote($quote); + $product = $this->product->get('simple'); + $item = $this->item->setProduct($product); + $item->setQuote($this->quote); // assure that default renderer was obtained $this->assertEquals( 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( + Xpath::getElementsCountForXpath( '//*[contains(@class,"product") and contains(@class,"name")]/a', - $this->_block->getRowItemHtml($item) + $this->block->getRowItemHtml($item) + ) + ); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + */ + public function testLinkOptionalProductFileItemHtml() + { + $quote = $this->quote->load('customer_quote_product_custom_options', 'reserved_order_id'); + $item = current($quote->getAllItems()); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + '//dd/a[contains(text(), "test.jpg")]', + $this->block->getRowItemHtml($item) ) ); } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php index 97f59e94d9cfe..719d78b07ca3c 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php @@ -205,4 +205,22 @@ public function testCustomerWithZeroStoreIdIsSubscribed() $this->assertEquals($customer->getId(), (int)$subscriber->getCustomerId()); $this->assertEquals($currentStore, (int)$subscriber->getStoreId()); } + + /** + * Test get list customer, which have more then 2 subscribes in newsletter_subscriber. + * + * @magentoAppArea frontend + * @magentoDataFixture Magento/Newsletter/_files/subscribers.php + */ + public function testCustomerWithTwoNewsLetterSubscriptions() + { + /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); + $searchCriteria = $searchBuilder->addFilter('entity_id', 1)->create(); + $items = $this->customerRepository->getList($searchCriteria)->getItems(); + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $items[0]; + $extensionAttributes = $customer->getExtensionAttributes(); + $this->assertTrue($extensionAttributes->getIsSubscribed()); + } } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php index 287564722ced6..b28788bc649a1 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php @@ -93,7 +93,6 @@ public function testProcessPortuguese() $secondStore = $storeRepository->get('fixture_second_store'); // check if Portuguese language is specified for the second store - CacheCleaner::cleanAll(); $storeResolver = $this->_objectManager->get(Resolver::class); $storeResolver->emulate($secondStore->getId()); $this->assertEquals('pt_BR', $storeResolver->getLocale()); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index 081cae5f98ee5..94cf6ef108474 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -737,4 +737,28 @@ private function getCustomerDataArray(): array CustomerInterface::WEBSITE_ID => 1, ]; } + + /** + * @magentoConfigFixture current_store sales/minimum_order/active 1 + * @magentoConfigFixture current_store sales/minimum_order/amount 5 + * @magentoConfigFixture current_store sales/minimum_order/tax_including 1 + * @magentoConfigFixture current_store sales/minimum_order/include_discount_amount 1 + * @magentoConfigFixture current_store tax/calculation/price_includes_tax 1 + * @magentoConfigFixture current_store tax/calculation/apply_after_discount 1 + * @magentoConfigFixture current_store tax/calculation/cross_border_trade_enabled 1 + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_with_coupon_5_off_no_condition.php + * @magentoDataFixture Magento/Tax/_files/tax_rule_region_1_al.php + * @magentoDataFixture Magento/Checkout/_files/quote_with_taxable_product_and_customer.php + */ + public function testValidateMinimumAmountWithPriceInclTaxAndDiscount() + { + /** @var $quote \Magento\Quote\Model\Quote */ + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_taxable_product'); + $quote->setCouponCode('CART_FIXED_DISCOUNT_5'); + $quote->collectTotals(); + $this->assertEquals(-5, $quote->getShippingAddress()->getBaseDiscountAmount()); + $this->assertEquals(9.3, $quote->getShippingAddress()->getBaseSubtotal()); + $this->assertEquals(5, $quote->getShippingAddress()->getBaseGrandTotal()); + $this->assertTrue($quote->validateMinimumAmount()); + } } diff --git a/dev/tests/integration/testsuite/Magento/ReleaseNotification/Controller/Adminhtml/Dashboard/IndexTest.php b/dev/tests/integration/testsuite/Magento/ReleaseNotification/Controller/Adminhtml/Dashboard/IndexTest.php index 9358d761b28b2..ffe2a4d6ca1c5 100644 --- a/dev/tests/integration/testsuite/Magento/ReleaseNotification/Controller/Adminhtml/Dashboard/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/ReleaseNotification/Controller/Adminhtml/Dashboard/IndexTest.php @@ -34,6 +34,7 @@ protected function setUp(): void protected function tearDown(): void { $this->objectManager->removeSharedInstance(ContentProviderInterface::class); + CacheCleaner::clean(['layout']); parent::tearDown(); } @@ -44,7 +45,6 @@ public function testExecute() { $content = include __DIR__ . '/../../../_files/validContent.php'; - CacheCleaner::cleanAll(); $this->contentProviderMock->expects($this->any()) ->method('getContent') ->willReturn($content); @@ -62,7 +62,6 @@ public function testExecute() public function testExecuteEmptyContent() { - CacheCleaner::cleanAll(); $this->contentProviderMock->expects($this->any()) ->method('getContent') ->willReturn('[]'); @@ -77,7 +76,6 @@ public function testExecuteEmptyContent() public function testExecuteFalseContent() { - CacheCleaner::cleanAll(); $this->contentProviderMock->expects($this->any()) ->method('getContent') ->willReturn(false); diff --git a/dev/tests/integration/testsuite/Magento/Review/Ui/DataProvider/Product/ReviewDataProviderTest.php b/dev/tests/integration/testsuite/Magento/Review/Ui/DataProvider/Product/ReviewDataProviderTest.php new file mode 100644 index 0000000000000..5cd594a37d0e0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Ui/DataProvider/Product/ReviewDataProviderTest.php @@ -0,0 +1,138 @@ + 'review_listing_data_source', + 'primaryFieldName' => 'review_id', + 'requestFieldName' => 'entity_id', + ]; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Sorting dataProvider test + * + * @magentoDataFixture Magento/Review/_files/different_reviews.php + * @dataProvider sortingDataProvider + * + * @param string $field + * @param string $direction + * @param array $expectedSortedTitles + * @return void + */ + public function testSorting(string $field, string $direction, array $expectedSortedTitles): void + { + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('current_product_id', 1); + + $dataProvider = $this->objectManager->create( + ReviewDataProvider::class, + array_merge($this->modelParams, ['request' => $request]) + ); + $dataProvider->addOrder($field, $direction); + $result = $dataProvider->getData(); + + $this->assertEquals($this->getItemsField($result, 'title'), $expectedSortedTitles); + } + + /** + * Return items field data + * + * @param array $arrItems + * @param string $field + * @return array + */ + private function getItemsField(array $arrItems, string $field): array + { + $data = []; + foreach ($arrItems['items'] as $review) { + $data[] = $review[$field]; + } + + return $data; + } + + /** + * DataProvider for testSorting + * + * @return array + */ + public function sortingDataProvider(): array + { + return [ + 'sort by title field ascending' => [ + 'title', + 'asc', + ['1 filter second review', '2 filter first review', 'Review Summary'], + ], + 'sort by title field descending' => [ + 'title', + 'desc', + ['Review Summary', '2 filter first review', '1 filter second review'], + ], + ]; + } + + /** + * Filter dataProvider test + * + * @magentoDataFixture Magento/Review/_files/different_reviews.php + * + * @return void + */ + public function testFilter(): void + { + $searchTitle = '2 filter first review'; + + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('current_product_id', 1); + + /** @var ReviewDataProvider $dataProvider */ + $dataProvider = $this->objectManager->create( + ReviewDataProvider::class, + array_merge($this->modelParams, ['request' => $request]) + ); + + /** @var Filter $filter */ + $filter = $this->objectManager->create(Filter::class); + $filter->setField('title') + ->setValue($searchTitle); + + $dataProvider->addFilter($filter); + $result = $dataProvider->getData(); + + $this->assertEquals($this->getItemsField($result, 'title'), [$searchTitle]); + } +} 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 index b6aa44bac1c4d..529b491269643 100644 --- 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 @@ -16,6 +16,7 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Wishlist\Model\Wishlist; /** * Class checks create order load block controller. @@ -201,6 +202,35 @@ public function testMoveFromOrderToShoppingCart(): void $this->quoteIdsToRemove[] = $customerCart->getId(); } + /** + * Check that Wishlist item is deleted after it has been added to Order. + * + * @return void + * @magentoDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testAddProductToOrderFromWishList(): void + { + /** @var Wishlist $wishlist */ + $wishlist = $this->_objectManager->create(Wishlist::class); + $wishlistItems = $wishlist->loadByCustomerId(1)->getItemCollection(); + $this->assertCount(1, $wishlistItems); + + $post = $this->hydratePost([ + 'sidebar' => [ + 'add_wishlist_item' => [ + $wishlistItems->getFirstItem()->getId() => 1, + ], + ], + ]); + $params = $this->hydrateParams(); + $this->dispatchWitParams($params, $post); + + $wishlistItems->clear()->load(); + $this->assertEmpty($wishlistItems); + $quoteItems = $this->session->getQuote()->getItemsCollection(); + $this->assertCount(1, $quoteItems); + } + /** * Check customer quotes * 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 3c26a53424d81..a96eac375e9f1 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 @@ -97,15 +97,21 @@ protected function prepareRequest(array $postParams = [], array $params = []): v * @param array $items * @param string $commentText * @param bool $doShipment + * @param bool $sendEmail * @return array */ - protected function hydratePost(array $items, string $commentText = '', $doShipment = false): array - { + protected function hydratePost( + array $items, + string $commentText = '', + $doShipment = false, + $sendEmail = false + ): array { return [ 'invoice' => [ 'items' => $items, 'comment_text' => $commentText, - 'do_shipment' => $doShipment + 'do_shipment' => $doShipment, + 'send_email' => $sendEmail ], ]; } 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 d00b4c784110c..027fc3d88ee49 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 @@ -58,6 +58,26 @@ public function testSendEmailOnInvoiceSave(): void $this->dispatch('backend/sales/order_invoice/save'); $invoice = $this->getInvoiceByOrder($order); $this->checkSuccess($invoice, 2); + $this->assertNull($this->transportBuilder->getSentMessage()); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testSendEmailOnInvoiceSaveEmailCopyOfInvoice(): void + { + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $post = $this->hydratePost([$itemId => 2], "", false, "1"); + $this->prepareRequest( + $post, + ['order_id' => $order->getEntityId()] + ); + $this->dispatch('backend/sales/order_invoice/save'); + $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(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Report/RuleTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Report/RuleTest.php new file mode 100644 index 0000000000000..58b82375cfbe9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Report/RuleTest.php @@ -0,0 +1,37 @@ +create(Order::class); + $order->loadByIncrementId($orderIncrementId) + ->setCouponRuleName($ruleName) + ->save(); + /** @var Rule $reportResource */ + $reportResource = $objectManager->create(Rule::class); + $reportResource->aggregate(); + $this->assertContains($ruleName, $reportResource->getUniqRulesNamesList()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php index 4ed096fa4418a..eebf5ea638d05 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php @@ -14,6 +14,9 @@ use Magento\SalesRule\Model\Coupon; use Magento\SalesRule\Model\ResourceModel\Coupon\Usage; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\MessageQueue\EnvironmentPreconditionException; +use Magento\TestFramework\MessageQueue\PreconditionFailedException; +use Magento\TestFramework\MessageQueue\PublisherConsumerController; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -23,9 +26,20 @@ * @magentoAppArea frontend * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CouponUsagesTest extends TestCase { + /** + * @var PublisherConsumerController + */ + private $publisherConsumerController; + + /** + * @var array + */ + private $consumers = ['sales.rule.update.coupon.usage']; + /** * @var ObjectManagerInterface */ @@ -61,12 +75,42 @@ protected function setUp(): void $this->couponUsage = $this->objectManager->get(DataObject::class); $this->quoteManagement = $this->objectManager->get(QuoteManagement::class); $this->orderService = $this->objectManager->get(OrderService::class); + + $this->publisherConsumerController = Bootstrap::getObjectManager()->create( + PublisherConsumerController::class, + [ + 'consumers' => $this->consumers, + 'logFilePath' => TESTS_TEMP_DIR . "/MessageQueueTestLog.txt", + 'maxMessages' => 100, + 'appInitParams' => Bootstrap::getInstance()->getAppInitParams() + ] + ); + try { + $this->publisherConsumerController->startConsumers(); + } catch (EnvironmentPreconditionException $e) { + $this->markTestSkipped($e->getMessage()); + } catch (PreconditionFailedException $e) { + $this->fail( + $e->getMessage() + ); + } + parent::setUp(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->publisherConsumerController->stopConsumers(); + parent::tearDown(); } /** * Test increasing coupon usages after after order placing and decreasing after order cancellation. * * @magentoDataFixture Magento/SalesRule/_files/coupons_limited_order.php + * @magentoDbIsolation disabled */ public function testSubmitQuoteAndCancelOrder() { @@ -74,6 +118,8 @@ public function testSubmitQuoteAndCancelOrder() $couponCode = 'one_usage'; $reservedOrderId = 'test01'; + $this->publisherConsumerController->startConsumers(); + /** @var Coupon $coupon */ $coupon = $this->objectManager->get(Coupon::class); $coupon->loadByCode($couponCode); @@ -83,6 +129,7 @@ public function testSubmitQuoteAndCancelOrder() // Make sure coupon usages value is incremented then order is placed. $order = $this->quoteManagement->submit($quote); + sleep(10); // timeout to processing Magento queue $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $coupon->getId()); $coupon->loadByCode($couponCode); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_5_off_no_condition.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_5_off_no_condition.php new file mode 100644 index 0000000000000..6ce2e0789e478 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_5_off_no_condition.php @@ -0,0 +1,49 @@ +get(Collection::class); +$customerGroupIds = $groupCollection->getAllIds(); +/** @var Rule $salesRule */ +$salesRule = $objectManager->create(Rule::class); +$salesRule->setData( + [ + 'name' => 'cart_rule_with_coupon_5_off_no_condition', + 'is_active' => 1, + 'customer_group_ids' => $groupCollection->getAllIds(), + 'coupon_type' => Rule::COUPON_TYPE_SPECIFIC, + 'simple_action' => Rule::CART_FIXED_ACTION, + 'discount_amount' => 5, + 'discount_step' => 0, + 'stop_rules_processing' => 1, + 'website_ids' => [ + $objectManager->get(StoreManagerInterface::class)->getWebsite()->getId(), + ], + 'store_labels' => [ + + 'store_id' => 0, + 'store_label' => 'cart_rule_with_coupon_5_off_no_condition', + ] + ] +); +$objectManager->get(\Magento\SalesRule\Model\ResourceModel\Rule::class)->save($salesRule); + +// Create coupon and assign "5$ fixed discount" rule to this coupon. +$coupon = $objectManager->create(Coupon::class); +$coupon->setRuleId($salesRule->getId()) + ->setCode('CART_FIXED_DISCOUNT_5') + ->setType(0); +$objectManager->get(CouponRepositoryInterface::class)->save($coupon); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_5_off_no_condition_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_5_off_no_condition_rollback.php new file mode 100644 index 0000000000000..7ecf7a915b1ea --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_with_coupon_5_off_no_condition_rollback.php @@ -0,0 +1,36 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteria = $searchCriteriaBuilder->addFilter('name', 'cart_rule_with_coupon_5_off_no_condition') + ->create(); +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = $objectManager->get(RuleRepositoryInterface::class); +$items = $ruleRepository->getList($searchCriteria) + ->getItems(); +/** @var Rule $salesRule */ +$salesRule = array_pop($items); +if ($salesRule !== null) { + $ruleRepository->deleteById($salesRule->getRuleId()); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreResolverTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreResolverTest.php index 53c58cc693a6e..c20b0ed27bf52 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreResolverTest.php @@ -28,7 +28,6 @@ public function testGetStoreData() $storeResolver = $this->objectManager->get(\Magento\Store\Model\StoreResolver::class); $storesDataRead = $methodReadStoresData->invoke($storeResolver); - CacheCleaner::cleanAll(); $storesData = $methodGetStoresData->invoke($storeResolver); $storesDataCached = $methodGetStoresData->invoke($storeResolver); $this->assertEquals($storesDataRead, $storesData); diff --git a/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/ThemeProviderTest.php b/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/ThemeProviderTest.php index 2f3d7065e5339..e2895a478b8d9 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/ThemeProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/ThemeProviderTest.php @@ -33,7 +33,6 @@ protected function setUp(): void $this->themeProviderOne = $objectManager->create(ThemeProvider::class); $this->themeProviderTwo = clone $this->themeProviderOne; $this->themeCollection = $objectManager->create(ThemeCollection::class); - CacheCleaner::clean(); } public function testGetThemeById() diff --git a/dev/tests/integration/testsuite/Magento/Translation/Model/Js/PreProcessorTest.php b/dev/tests/integration/testsuite/Magento/Translation/Model/Js/PreProcessorTest.php index 4bc1c5b55ade5..842912546ece8 100644 --- a/dev/tests/integration/testsuite/Magento/Translation/Model/Js/PreProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Translation/Model/Js/PreProcessorTest.php @@ -82,7 +82,6 @@ protected function tearDown(): void */ public function testProcess(string $content, string $translation) { - CacheCleaner::cleanAll(); $this->assertEquals($translation, $this->model->translate($content)); } diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php index a9ce1d4b181a9..6d635d01b2f48 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/StoreSwitcher/RewriteUrlTest.php @@ -65,8 +65,8 @@ protected function setUp(): void /** * Test switching stores with non-existent cms pages and then redirecting to the homepage * - * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php * @magentoDataFixture Magento/Catalog/_files/category_product.php + * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrite.php * @magentoDbIsolation disabled * @magentoAppIsolation enabled * @return void diff --git a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php index 90b1706ed4e22..e20c4a4b60e62 100644 --- a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php @@ -7,18 +7,30 @@ namespace Magento\User\Model; use Magento\Authorization\Model\Role; +use Magento\Email\Model\ResourceModel\Template\Collection as TemplateCollection; use Magento\Framework\App\CacheInterface; +use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\Encryption\Encryptor; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NotFoundException; +use Magento\Framework\Exception\State\UserLockedException; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Phrase; use Magento\Framework\Stdlib\DateTime; +use Magento\TestFramework\Bootstrap as TestFrameworkBootstrap; +use Magento\TestFramework\Entity; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\User\Model\User as UserModel; +use PHPUnit\Framework\TestCase; /** * @magentoAppArea adminhtml * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class UserTest extends \PHPUnit\Framework\TestCase +class UserTest extends TestCase { /** * @var UserModel @@ -74,12 +86,12 @@ public function testCRUD() )->setUsername( 'user2' )->setPassword( - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestFrameworkBootstrap::ADMIN_PASSWORD )->setEmail( 'user@magento.com' ); - $crud = new \Magento\TestFramework\Entity($this->_model, ['firstname' => '_New_name_']); + $crud = new Entity($this->_model, ['firstname' => '_New_name_']); $crud->testCrud(); } @@ -103,7 +115,7 @@ public function testLoadByUsername() { $this->_model->loadByUsername('non_existing_user'); $this->assertNull($this->_model->getId(), 'The admin user has an unexpected ID'); - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $this->assertNotEmpty($this->_model->getId(), 'The admin user should have been loaded'); } @@ -114,8 +126,8 @@ public function testLoadByUsername() */ public function testUpdateRoleOnSave() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); - $this->assertEquals(\Magento\TestFramework\Bootstrap::ADMIN_ROLE_NAME, $this->_model->getRole()->getRoleName()); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); + $this->assertEquals(TestFrameworkBootstrap::ADMIN_ROLE_NAME, $this->_model->getRole()->getRoleName()); $this->_model->setRoleId(self::$_newRole->getId())->save(); $this->assertEquals('admin_role', $this->_model->getRole()->getRoleName()); } @@ -137,9 +149,9 @@ public static function roleDataFixture() */ public function testSaveExtra() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $this->_model->saveExtra(['test' => 'val']); - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $extra = $this->_model->getExtra(); $this->assertEquals($extra['test'], 'val'); } @@ -149,10 +161,10 @@ public function testSaveExtra() */ public function testGetRoles() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $roles = $this->_model->getRoles(); $this->assertCount(1, $roles); - $this->assertEquals(\Magento\TestFramework\Bootstrap::ADMIN_ROLE_NAME, $this->_model->getRole()->getRoleName()); + $this->assertEquals(TestFrameworkBootstrap::ADMIN_ROLE_NAME, $this->_model->getRole()->getRoleName()); $this->_model->setRoleId(self::$_newRole->getId())->save(); $roles = $this->_model->getRoles(); $this->assertCount(1, $roles); @@ -164,10 +176,10 @@ public function testGetRoles() */ public function testGetRole() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $role = $this->_model->getRole(); $this->assertInstanceOf(Role::class, $role); - $this->assertEquals(\Magento\TestFramework\Bootstrap::ADMIN_ROLE_NAME, $this->_model->getRole()->getRoleName()); + $this->assertEquals(TestFrameworkBootstrap::ADMIN_ROLE_NAME, $this->_model->getRole()->getRoleName()); $this->_model->setRoleId(self::$_newRole->getId())->save(); $role = $this->_model->getRole(); $this->assertEquals(self::$_newRole->getId(), $role->getId()); @@ -178,7 +190,7 @@ public function testGetRole() */ public function testDeleteFromRole() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $roles = $this->_model->getRoles(); $this->_model->setRoleId(reset($roles))->deleteFromRole(); $role = $this->_model->getRole(); @@ -187,7 +199,7 @@ public function testDeleteFromRole() public function testRoleUserExists() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $role = $this->_model->getRole(); $this->_model->setRoleId($role->getId()); $this->assertTrue($this->_model->roleUserExists()); @@ -198,16 +210,16 @@ public function testRoleUserExists() public function testGetCollection() { $this->assertInstanceOf( - \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection::class, + AbstractCollection::class, $this->_model->getCollection() ); } public function testGetName() { - $firstname = \Magento\TestFramework\Bootstrap::ADMIN_FIRSTNAME; - $lastname = \Magento\TestFramework\Bootstrap::ADMIN_LASTNAME; - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $firstname = TestFrameworkBootstrap::ADMIN_FIRSTNAME; + $lastname = TestFrameworkBootstrap::ADMIN_LASTNAME; + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $this->assertEquals("$firstname $lastname", $this->_model->getName()); $this->assertEquals("$firstname///$lastname", $this->_model->getName('///')); } @@ -226,11 +238,11 @@ public function testGetUninitializedAclRole() */ public function testAuthenticate() { - $this->assertFalse($this->_model->authenticate('User', \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD)); + $this->assertFalse($this->_model->authenticate('User', TestFrameworkBootstrap::ADMIN_PASSWORD)); $this->assertTrue( $this->_model->authenticate( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestFrameworkBootstrap::ADMIN_NAME, + TestFrameworkBootstrap::ADMIN_PASSWORD ) ); } @@ -242,11 +254,11 @@ public function testAuthenticate() */ public function testAuthenticateCaseInsensitive() { - $this->assertTrue($this->_model->authenticate('user', \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD)); + $this->assertTrue($this->_model->authenticate('user', TestFrameworkBootstrap::ADMIN_PASSWORD)); $this->assertTrue( $this->_model->authenticate( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestFrameworkBootstrap::ADMIN_NAME, + TestFrameworkBootstrap::ADMIN_PASSWORD ) ); } @@ -256,13 +268,13 @@ public function testAuthenticateCaseInsensitive() */ public function testAuthenticateInactiveUser() { - $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->expectException(AuthenticationException::class); $this->_model->load(1); $this->_model->setIsActive(0)->save(); $this->_model->authenticate( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestFrameworkBootstrap::ADMIN_NAME, + TestFrameworkBootstrap::ADMIN_PASSWORD ); } @@ -272,14 +284,14 @@ public function testAuthenticateInactiveUser() */ public function testAuthenticateUserWithoutRole() { - $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->expectException(AuthenticationException::class); $this->_model->loadByUsername('customRoleUser'); $roles = $this->_model->getRoles(); $this->_model->setRoleId(reset($roles))->deleteFromRole(); $this->_model->authenticate( 'customRoleUser', - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestFrameworkBootstrap::ADMIN_PASSWORD ); } @@ -289,13 +301,13 @@ public function testAuthenticateUserWithoutRole() */ public function testLoginsAreLogged() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $lognum = $this->_model->getLognum(); $beforeLogin = time(); $this->_model->login( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestFrameworkBootstrap::ADMIN_NAME, + TestFrameworkBootstrap::ADMIN_PASSWORD )->reload(); $loginTime = strtotime($this->_model->getLogdate()); @@ -304,8 +316,8 @@ public function testLoginsAreLogged() $beforeLogin = time(); $this->_model->login( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, - \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + TestFrameworkBootstrap::ADMIN_NAME, + TestFrameworkBootstrap::ADMIN_PASSWORD )->reload(); $loginTime = strtotime($this->_model->getLogdate()); $this->assertTrue($beforeLogin <= $loginTime && $loginTime <= time()); @@ -314,11 +326,11 @@ public function testLoginsAreLogged() public function testReload() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $this->_model->setFirstname('NewFirstName'); $this->assertEquals('NewFirstName', $this->_model->getFirstname()); $this->_model->reload(); - $this->assertEquals(\Magento\TestFramework\Bootstrap::ADMIN_FIRSTNAME, $this->_model->getFirstname()); + $this->assertEquals(TestFrameworkBootstrap::ADMIN_FIRSTNAME, $this->_model->getFirstname()); } /** @@ -326,7 +338,7 @@ public function testReload() */ public function testHasAssigned2Role() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $role = $this->_model->hasAssigned2Role($this->_model); $this->assertCount(1, $role); $this->assertArrayHasKey('role_id', $role[0]); @@ -348,7 +360,7 @@ public function testBeforeSaveRequiredFieldsValidation() . 'Password is required field.' . PHP_EOL . 'Invalid type given. String expected' . PHP_EOL . 'Invalid type given. String, integer or float expected'; - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage($expectedMessages); $this->_model->setSomething('some_value'); @@ -402,7 +414,7 @@ public function testBeforeSavePasswordHash() */ public function testBeforeSavePasswordsDoNotMatch() { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('Your password confirmation must match your password.'); $this->_model->setPassword('password2'); @@ -415,7 +427,7 @@ public function testBeforeSavePasswordsDoNotMatch() */ public function testBeforeSavePasswordTooShort() { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('Your password must include both numeric and alphabetic characters.'); $this->_model->setPassword('123456'); @@ -429,7 +441,7 @@ public function testBeforeSavePasswordTooShort() */ public function testBeforeSavePasswordInsecure($password) { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('Your password must include both numeric and alphabetic characters.'); $this->_model->setPassword($password); @@ -446,7 +458,7 @@ public function beforeSavePasswordInsecureDataProvider() */ public function testBeforeSaveUserIdentityViolation() { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('A user with the same user name or email already exists.'); $this->_model->setUsername('user'); @@ -479,12 +491,12 @@ public function testBeforeSaveValidationSuccess() */ public function testChangeResetPasswordLinkToken() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $this->_model->changeResetPasswordLinkToken('test'); $date = $this->_model->getRpTokenCreatedAt(); $this->assertNotNull($date); $this->_model->save(); - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $this->assertEquals('test', $this->_model->getRpToken()); $this->assertEquals(strtotime($date), strtotime($this->_model->getRpTokenCreatedAt())); } @@ -496,11 +508,11 @@ public function testChangeResetPasswordLinkToken() */ public function testIsResetPasswordLinkTokenExpired() { - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $this->assertTrue($this->_model->isResetPasswordLinkTokenExpired()); $this->_model->changeResetPasswordLinkToken('test'); $this->_model->save(); - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername(TestFrameworkBootstrap::ADMIN_NAME); $this->assertFalse($this->_model->isResetPasswordLinkTokenExpired()); $this->_model->setRpTokenCreatedAt($this->_dateTime->formatDate(time() - 60 * 60 * 2 + 2)); $this->assertFalse($this->_model->isResetPasswordLinkTokenExpired()); @@ -526,7 +538,7 @@ public function testGetSetHasAvailableResources() public function testPerformIdentityCheck() { $this->_model->loadByUsername('adminUser'); - $passwordString = \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD; + $passwordString = TestFrameworkBootstrap::ADMIN_PASSWORD; $this->_model->performIdentityCheck($passwordString); } @@ -537,7 +549,7 @@ public function testPerformIdentityCheck() */ public function testPerformIdentityCheckWrongPassword() { - $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); + $this->expectException(AuthenticationException::class); $this->_model->loadByUsername('adminUser'); $passwordString = 'wrongPassword'; @@ -555,14 +567,80 @@ public function testPerformIdentityCheckWrongPassword() */ public function testPerformIdentityCheckLockExpires() { - $this->expectException(\Magento\Framework\Exception\State\UserLockedException::class); + $this->expectException(UserLockedException::class); $this->_model->loadByUsername('adminUser2'); - $this->_model->performIdentityCheck(\Magento\TestFramework\Bootstrap::ADMIN_PASSWORD); + $this->_model->performIdentityCheck(TestFrameworkBootstrap::ADMIN_PASSWORD); $this->expectExceptionMessage( 'The account sign-in was incorrect or your account is disabled temporarily. ' . 'Please wait and try again later.' ); } + + /** + * Verify custom notification is sent when new user created + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Email/Model/_files/email_template_new_user_notification.php + */ + public function testSendNotificationEmailsIfRequired() + { + /** @var MutableScopeConfigInterface $config */ + $config = Bootstrap::getObjectManager() + ->get(MutableScopeConfigInterface::class); + $config->setValue( + 'admin/emails/new_user_notification_template', + $this->getCustomEmailTemplateIdForNewUserNotification() + ); + $userModel = Bootstrap::getObjectManager() + ->create(User::class); + $userModel->setFirstname( + 'John' + )->setLastname( + 'Doe' + )->setUsername( + 'user2' + )->setPassword( + TestFrameworkBootstrap::ADMIN_PASSWORD + )->setEmail( + 'user@magento.com' + ); + $userModel->save(); + $userModel->sendNotificationEmailsIfRequired(); + /** @var TransportBuilderMock $transportBuilderMock */ + $transportBuilderMock = Bootstrap::getObjectManager() + ->get(TransportBuilderMock::class); + $sentMessage = $transportBuilderMock->getSentMessage(); + $this->assertSame( + 'New User Notification Custom Text ' . $userModel->getFirstname() . ', ' . $userModel->getLastname(), + $sentMessage->getBodyText() + ); + } + + /** + * Return email template id for new user notification + * + * @return int|null + * @throws NotFoundException + */ + private function getCustomEmailTemplateIdForNewUserNotification(): ?int + { + $templateId = null; + $templateCollection = Bootstrap::getObjectManager() + ->get(TemplateCollection::class); + $origTemplateCode = 'admin_emails_new_user_notification_template'; + foreach ($templateCollection as $template) { + if ($template->getOrigTemplateCode() == $origTemplateCode) { + $templateId = (int) $template->getId(); + } + } + if ($templateId === null) { + throw new NotFoundException(new Phrase( + 'Customized %templateCode% email template not found', + ['templateCode' => $origTemplateCode] + )); + } + return $templateId; + } } diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/search/search.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/search/search.test.js index 11af4cdc219a9..87fb61873b653 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/search/search.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/search/search.test.js @@ -37,5 +37,15 @@ define([ searchObj.updatePreview(); expect(searchObj.updatePreview).toHaveBeenCalled(); }); + it('set the proper keywordUpdated value on new search keyword', function () { + searchObj.value = 'keyword 1'; + expect(searchObj.keywordUpdated).toEqual(false); + searchObj.apply('keyword 2'); + expect(searchObj.keywordUpdated).toEqual(true); + searchObj.apply('keyword 2'); + expect(searchObj.keywordUpdated).toEqual(false); + searchObj.apply('keyword 3'); + expect(searchObj.keywordUpdated).toEqual(true); + }); }); }); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php index 1c0f451de71dc..f57a29e9bda0d 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php @@ -266,8 +266,10 @@ private function assertAutoloadRegistrar(\StdClass $json, $dir) */ private function assertNoVersionSpecified(\StdClass $json) { - $errorMessage = 'Version must not be specified in the root and package composer JSON files in Git'; - $this->assertObjectNotHasAttribute('version', $json, $errorMessage); + if (!in_array($json->name, self::$rootComposerModuleBlacklist)) { + $errorMessage = 'Version must not be specified in the root and package composer JSON files in Git'; + $this->assertObjectNotHasAttribute('version', $json, $errorMessage); + } } /** diff --git a/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php b/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php index aa1f8b1c259a4..65358cd785066 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php @@ -209,14 +209,14 @@ private function getFullWhitelist() } /** - * Retrieves the lowest PHP version specified in composer.json of project. + * Retrieves the lowest and highest PHP version specified in composer.json of project. * - * @return string + * @return array */ - private function getLowestPhpVersion(): string + private function getTargetPhpVersions(): array { $composerJson = json_decode(file_get_contents(BP . '/composer.json'), true); - $phpVersion = '7.0'; + $versionsRange = []; if (isset($composerJson['require']['php'])) { $versions = explode('||', $composerJson['require']['php']); @@ -232,12 +232,17 @@ private function getLowestPhpVersion(): string //sort versions usort($versions, 'version_compare'); - $lowestVersion = array_shift($versions); - $versionParts = explode('.', $lowestVersion); - $phpVersion = sprintf('%s.%s', $versionParts[0], $versionParts[1] ?? '0'); + $versionsRange[] = array_shift($versions); + if (!empty($versions)) { + $versionsRange[] = array_pop($versions); + } + foreach ($versionsRange as $key => $version) { + $versionParts = explode('.', $versionsRange[$key]); + $versionsRange[$key] = sprintf('%s.%s', $versionParts[0], $versionParts[1] ?? '0'); + } } - return $phpVersion; + return $versionsRange; } /** @@ -408,7 +413,8 @@ public function testStrictTypes() */ public function testPhpCompatibility() { - $targetVersion = $this->getLowestPhpVersion(); + $targetVersions = $this->getTargetPhpVersions(); + $this->assertNotEmpty($targetVersions, 'No supported versions information in composer.json'); $reportFile = self::$reportDir . '/phpcompatibility_report.txt'; $rulesetDir = __DIR__ . '/_files/PHPCompatibilityMagento'; @@ -417,7 +423,11 @@ public function testPhpCompatibility() } $codeSniffer = new PhpCompatibility($rulesetDir, $reportFile, new Wrapper()); - $codeSniffer->setTestVersion($targetVersion); + if (count($targetVersions) > 1) { + $codeSniffer->setTestVersion($targetVersions[0] . '-' . $targetVersions[1]); + } else { + $codeSniffer->setTestVersion($targetVersions[0]); + } $result = $codeSniffer->run( $this->isFullScan() ? $this->getFullWhitelist() : self::getWhitelist(['php', 'phtml']) diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt index 8ea162d3b1394..6b0007f58830b 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/blacklist/common.txt @@ -9,6 +9,7 @@ lib/internal/Magento/Framework/Interception/Test/Unit/Code/Generator/Interceptor lib/internal/Magento/Framework/Interception/Test/Unit/Config/ConfigTest.php dev/tests/integration/framework/deployTestModules.php dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php +dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php dev/tests/integration/testsuite/Magento/Framework/Filter/DirectiveProcessor/SimpleDirectiveTest.php dev/tests/integration/testsuite/Magento/Framework/Session/ConfigTest.php dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon index d487fbe602acf..0bdb5861dbf33 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon @@ -1,41 +1,41 @@ parameters: - checkExplicitMixedMissingReturn: true - checkPhpDocMissingReturn: true - reportUnmatchedIgnoredErrors: false - excludes_analyse: - - %rootDir%/../../../lib/internal/Magento/Framework/ObjectManager/Test/Unit/* - - %rootDir%/../../../*/_files/* - - %rootDir%/../../../dev/tests/*/Fixtures/* - - %rootDir%/../../../dev/tests/*/tmp/* - - %rootDir%/../../../dev/tests/*/_generated/* - - %rootDir%/../../../pub/* - autoload_directories: - - %rootDir%/../../../dev/tests/static/framework/tests/unit/testsuite/Magento - - %rootDir%/../../../dev/tests/integration/framework/tests/unit/testsuite/Magento - - %rootDir%/../../../dev/tests/api-functional/_files/Magento - autoload_files: - - %rootDir%/../../../dev/tests/static/framework/autoload.php - - %rootDir%/../../../dev/tests/integration/framework/autoload.php - - %rootDir%/../../../dev/tests/api-functional/framework/autoload.php - - %rootDir%/../../../dev/tests/setup-integration/framework/autoload.php - - %rootDir%/../../../dev/tests/static/framework/Magento/PhpStan/autoload.php - ignoreErrors: - # Ignore PHPStan\Rules\Classes\UnusedConstructorParametersRule - - '#Constructor of class [a-zA-Z0-9\\_]+ has an unused parameter#' - # Ignore setCustomErrorHandler function not found in bootstrap files - - '#Function setCustomErrorHandler not found#' - # Ignore 'return statement is missing' error when 'void' is present in return type list - - '#Method (?:.*?) should return (?:.*?)void(?:.*?) but return statement is missing.#' - # Ignore constants, defined dynamically. - - '#Constant TESTS_WEB_API_ADAPTER not found.#' - - '#Constant TESTS_BASE_URL not found.#' - - '#Constant TESTS_XDEBUG_ENABLED not found.#' - - '#Constant TESTS_XDEBUG_SESSION not found.#' - - '#Constant INTEGRATION_TESTS_DIR not found.#' - - '#Constant MAGENTO_MODULES_PATH not found.#' - - '#Constant TESTS_MODULES_PATH not found.#' - - '#Constant TESTS_INSTALLATION_DB_CONFIG_FILE not found.#' - - '#Constant T_[A-Z_]+ not found.#' + checkExplicitMixedMissingReturn: true + checkPhpDocMissingReturn: true + reportUnmatchedIgnoredErrors: false + excludes_analyse: + - %rootDir%/../../../lib/internal/Magento/Framework/ObjectManager/Test/Unit/* + - %rootDir%/../../../*/_files/* + - %rootDir%/../../../dev/tests/*/Fixtures/* + - %rootDir%/../../../dev/tests/*/tmp/* + - %rootDir%/../../../dev/tests/*/_generated/* + - %rootDir%/../../../pub/* + autoload_directories: + - %rootDir%/../../../dev/tests/static/framework/tests/unit/testsuite/Magento + - %rootDir%/../../../dev/tests/integration/framework/tests/unit/testsuite/Magento + - %rootDir%/../../../dev/tests/api-functional/_files/Magento + autoload_files: + - %rootDir%/../../../dev/tests/static/framework/autoload.php + - %rootDir%/../../../dev/tests/integration/framework/autoload.php + - %rootDir%/../../../dev/tests/api-functional/framework/autoload.php + - %rootDir%/../../../dev/tests/setup-integration/framework/autoload.php + - %rootDir%/../../../dev/tests/static/framework/Magento/PhpStan/autoload.php + ignoreErrors: + # Ignore PHPStan\Rules\Classes\UnusedConstructorParametersRule + - '#Constructor of class [a-zA-Z0-9\\_]+ has an unused parameter#' + # Ignore setCustomErrorHandler function not found in bootstrap files + - '#Function setCustomErrorHandler not found#' + # Ignore 'return statement is missing' error when 'void' is present in return type list + - '#Method (?:.*?) should return (?:.*?)void(?:.*?) but return statement is missing.#' + # Ignore constants, defined dynamically. + - '#Constant TESTS_WEB_API_ADAPTER not found.#' + - '#Constant TESTS_BASE_URL not found.#' + - '#Constant TESTS_XDEBUG_ENABLED not found.#' + - '#Constant TESTS_XDEBUG_SESSION not found.#' + - '#Constant INTEGRATION_TESTS_DIR not found.#' + - '#Constant MAGENTO_MODULES_PATH not found.#' + - '#Constant TESTS_MODULES_PATH not found.#' + - '#Constant TESTS_INSTALLATION_DB_CONFIG_FILE not found.#' + - '#Constant T_[A-Z_]+ not found.#' services: diff --git a/lib/internal/Magento/Framework/App/Bootstrap.php b/lib/internal/Magento/Framework/App/Bootstrap.php index 93d5535d0e10e..83f292cfdf703 100644 --- a/lib/internal/Magento/Framework/App/Bootstrap.php +++ b/lib/internal/Magento/Framework/App/Bootstrap.php @@ -13,6 +13,7 @@ use Magento\Framework\Autoload\Populator; use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\HTTP\PhpEnvironment\Response; use Psr\Log\LoggerInterface; /** @@ -386,7 +387,7 @@ private function initErrorHandler() $handler = new ErrorHandler(); set_error_handler([$handler, 'handler']); } - + /** * Getter for error code * @@ -428,9 +429,13 @@ public function isDeveloperMode() */ protected function terminate(\Throwable $e) { - + /** @var Response $response */ + $response = $this->objectManager->get(Response::class); + $response->clearHeaders(); + $response->setHttpResponseCode(500); + $response->setHeader('Content-Type', 'text/plain'); if ($this->isDeveloperMode()) { - echo $e; + $response->setBody($e); } else { $message = "An error has happened during application run. See exception log for details.\n"; try { @@ -441,8 +446,9 @@ protected function terminate(\Throwable $e) } catch (\Exception $e) { $message .= "Could not write error message to log. Please use developer mode to see the message.\n"; } - echo $message; + $response->setBody($message); } + $response->sendResponse(); exit(1); } // phpcs:enable diff --git a/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader.php b/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader.php deleted file mode 100644 index 3c5e9ca2539dc..0000000000000 --- a/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader.php +++ /dev/null @@ -1,53 +0,0 @@ - 'name' - ]; - - /** - * @param \Magento\Framework\Config\FileResolverInterface $fileResolver - * @param \Magento\Framework\Communication\Config\Reader\XmlReader\Converter $converter - * @param \Magento\Framework\Communication\Config\Reader\XmlReader\SchemaLocator $schemaLocator - * @param \Magento\Framework\Config\ValidationStateInterface $validationState - * @param string $fileName - * @param array $idAttributes - * @param string $domDocumentClass - * @param string $defaultScope - */ - public function __construct( - \Magento\Framework\Config\FileResolverInterface $fileResolver, - XmlReader\Converter $converter, - XmlReader\SchemaLocator $schemaLocator, - \Magento\Framework\Config\ValidationStateInterface $validationState, - $fileName = 'communication.xml', - $idAttributes = [], - $domDocumentClass = \Magento\Framework\Config\Dom::class, - $defaultScope = 'global' - ) { - parent::__construct( - $fileResolver, - $converter, - $schemaLocator, - $validationState, - $fileName, - $idAttributes, - $domDocumentClass, - $defaultScope - ); - } -} diff --git a/lib/internal/Magento/Framework/Mview/Config/Converter.php b/lib/internal/Magento/Framework/Mview/Config/Converter.php index 5c33ac150d00a..988cffa8b7ce2 100644 --- a/lib/internal/Magento/Framework/Mview/Config/Converter.php +++ b/lib/internal/Magento/Framework/Mview/Config/Converter.php @@ -5,10 +5,34 @@ */ namespace Magento\Framework\Mview\Config; +use Magento\Framework\Mview\View\AdditionalColumnsProcessor\DefaultProcessor; +use Magento\Framework\Mview\View\ChangeLogBatchWalker; use Magento\Framework\Mview\View\SubscriptionInterface; class Converter implements \Magento\Framework\Config\ConverterInterface { + /** + * @var string + */ + private $defaultProcessor; + + /** + * @var string + */ + private $defaultIterator; + + /** + * @param string $defaultProcessor + * @param string $defaultIterator + */ + public function __construct( + string $defaultProcessor = DefaultProcessor::class, + string $defaultIterator = ChangeLogBatchWalker::class + ) { + $this->defaultProcessor = $defaultProcessor; + $this->defaultIterator = $defaultIterator; + } + /** * Convert dom node tree to array * @@ -28,6 +52,7 @@ public function convert($source) $data['view_id'] = $viewId; $data['action_class'] = $this->getAttributeValue($viewNode, 'class'); $data['group'] = $this->getAttributeValue($viewNode, 'group'); + $data['walker'] = $this->getAttributeValue($viewNode, 'walker') ?: $this->defaultIterator; $data['subscriptions'] = []; /** @var $childNode \DOMNode */ @@ -76,6 +101,7 @@ protected function convertChild(\DOMNode $childNode, $data) $name = $this->getAttributeValue($subscription, 'name'); $column = $this->getAttributeValue($subscription, 'entity_column'); $subscriptionModel = $this->getAttributeValue($subscription, 'subscription_model'); + if (!empty($subscriptionModel) && !in_array( SubscriptionInterface::class, @@ -89,11 +115,44 @@ class_implements(ltrim($subscriptionModel, '\\')) $data['subscriptions'][$name] = [ 'name' => $name, 'column' => $column, - 'subscription_model' => $subscriptionModel + 'subscription_model' => $subscriptionModel, + 'additional_columns' => $this->getAdditionalColumns($subscription), + 'processor' => $this->getAttributeValue($subscription, 'processor') + ?: $this->defaultProcessor ]; } break; } return $data; } + + /** + * Retrieve additional columns of subscription table + * + * @param \DOMNode $subscription + * @return array + */ + private function getAdditionalColumns(\DOMNode $subscription): array + { + $additionalColumns = []; + foreach ($subscription->childNodes as $childNode) { + if ($childNode->nodeType != XML_ELEMENT_NODE || $childNode->nodeName != 'additionalColumns') { + continue; + } + + foreach ($childNode->childNodes as $columnNode) { + if ($columnNode->nodeName !== 'column') { + continue; + } + + $additionalColumns[$this->getAttributeValue($columnNode, 'name')] = [ + 'name' => $this->getAttributeValue($columnNode, 'name'), + 'cl_name' => $this->getAttributeValue($columnNode, 'cl_name'), + 'constant' => $this->getAttributeValue($columnNode, 'constant'), + ]; + } + } + + return $additionalColumns; + } } diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php index 0a4e8a3840749..16fc3a91924b8 100644 --- a/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php +++ b/lib/internal/Magento/Framework/Mview/Test/Unit/View/ChangelogTest.php @@ -11,6 +11,8 @@ use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\DB\Ddl\Table; use Magento\Framework\DB\Select; +use Magento\Framework\Mview\Config; +use Magento\Framework\Mview\View\AdditionalColumnsProcessor\ProcessorFactory; use Magento\Framework\Mview\View\Changelog; use Magento\Framework\Mview\View\ChangelogInterface; use PHPUnit\Framework\MockObject\MockObject; @@ -40,13 +42,33 @@ class ChangelogTest extends TestCase */ protected $resourceMock; + /** + * @var ProcessorFactory|MockObject + */ + protected $processorFactory; + protected function setUp(): void { $this->connectionMock = $this->createMock(Mysql::class); $this->resourceMock = $this->createMock(ResourceConnection::class); $this->mockGetConnection($this->connectionMock); + $this->processorFactory = $this->createMock(ProcessorFactory::class); + + $this->model = new Changelog($this->resourceMock, $this->getMviewConfigMock(), $this->processorFactory); + } - $this->model = new Changelog($this->resourceMock); + /** + * @return Config|MockObject + */ + private function getMviewConfigMock() + { + $mviewConfigMock = $this->createMock(Config::class); + $mviewConfigMock->expects($this->any()) + ->method('getView') + ->willReturn([ + 'subscriptions' => [] + ]); + return $mviewConfigMock; } public function testInstanceOf() @@ -54,7 +76,7 @@ public function testInstanceOf() $resourceMock = $this->createMock(ResourceConnection::class); $resourceMock->expects($this->once())->method('getConnection')->willReturn(true); - $model = new Changelog($resourceMock); + $model = new Changelog($resourceMock, $this->getMviewConfigMock(), $this->processorFactory); $this->assertInstanceOf(ChangelogInterface::class, $model); } @@ -65,7 +87,7 @@ public function testCheckConnectionException() $resourceMock = $this->createMock(ResourceConnection::class); $resourceMock->expects($this->once())->method('getConnection')->willReturn(null); - $model = new Changelog($resourceMock); + $model = new Changelog($resourceMock, $this->getMviewConfigMock(), $this->processorFactory); $model->setViewId('ViewIdTest'); $this->assertNull($model); } diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php index 697071d0d3067..3f3c326beb5a5 100644 --- a/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php +++ b/lib/internal/Magento/Framework/Mview/Test/Unit/View/SubscriptionTest.php @@ -7,10 +7,13 @@ namespace Magento\Framework\Mview\Test\Unit\View; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\DB\Ddl\Trigger; use Magento\Framework\DB\Ddl\TriggerFactory; +use Magento\Framework\Mview\Config; +use Magento\Framework\Mview\View\AdditionalColumnsProcessor\DefaultProcessor; use Magento\Framework\Mview\View\ChangelogInterface; use Magento\Framework\Mview\View\CollectionInterface; use Magento\Framework\Mview\View\StateInterface; @@ -46,8 +49,19 @@ class SubscriptionTest extends TestCase /** @var string */ private $tableName; + /** + * @var Config|MockObject + */ + private $mviewConfig; + + /** + * @var DefaultProcessor|MockObject + */ + private $defaultProcessor; + protected function setUp(): void { + $this->tableName = 'test_table'; $this->connectionMock = $this->createMock(Mysql::class); $this->resourceMock = $this->createMock(ResourceConnection::class); @@ -55,10 +69,14 @@ protected function setUp(): void ->method('quoteIdentifier') ->willReturnArgument(0); + $this->defaultProcessor = $this->createMock(DefaultProcessor::class); $this->resourceMock->expects($this->atLeastOnce()) ->method('getConnection') ->willReturn($this->connectionMock); - + ObjectManager::getInstance()->expects($this->any()) + ->method('get') + ->with(DefaultProcessor::class) + ->willReturn($this->defaultProcessor); $this->triggerFactoryMock = $this->createMock(TriggerFactory::class); $this->viewCollectionMock = $this->getMockForAbstractClass( CollectionInterface::class, @@ -78,18 +96,33 @@ protected function setUp(): void true, [] ); - + $this->viewMock->expects($this->any()) + ->method('getId') + ->willReturn(1); $this->resourceMock->expects($this->any()) ->method('getTableName') ->willReturnArgument(0); - + $mviewConfigMock = $this->createMock(Config::class); + $mviewConfigMock->expects($this->any()) + ->method('getView') + ->willReturn([ + 'subscriptions' => [ + $this->tableName => [ + 'processor' => DefaultProcessor::class + ] + ] + ]); + $this->mviewConfig = $mviewConfigMock; $this->model = new Subscription( $this->resourceMock, $this->triggerFactoryMock, $this->viewCollectionMock, $this->viewMock, $this->tableName, - 'columnName' + 'columnName', + [], + [], + $mviewConfigMock ); } @@ -122,7 +155,7 @@ public function testCreate() $triggerMock->expects($this->exactly(3)) ->method('setName') ->with($triggerName)->willReturnSelf(); - $triggerMock->expects($this->exactly(3)) + $triggerMock->expects($this->any()) ->method('getName') ->willReturn('triggerName'); $triggerMock->expects($this->exactly(3)) @@ -167,7 +200,7 @@ public function testCreate() true, [] ); - $changelogMock->expects($this->exactly(3)) + $changelogMock->expects($this->any()) ->method('getName') ->willReturn('test_view_cl'); $changelogMock->expects($this->exactly(3)) @@ -191,7 +224,7 @@ public function testCreate() true, [] ); - $otherChangelogMock->expects($this->exactly(3)) + $otherChangelogMock->expects($this->any()) ->method('getName') ->willReturn('other_test_view_cl'); $otherChangelogMock->expects($this->exactly(3)) @@ -217,7 +250,7 @@ public function testCreate() ->method('getChangelog') ->willReturn($otherChangelogMock); - $this->viewMock->expects($this->exactly(3)) + $this->viewMock->expects($this->any()) ->method('getId') ->willReturn('this_id'); $this->viewMock->expects($this->never()) @@ -235,7 +268,6 @@ public function testCreate() $this->connectionMock->expects($this->exactly(3)) ->method('createTrigger') ->with($triggerMock); - $this->model->create(); } @@ -244,7 +276,7 @@ public function testRemove() $triggerMock = $this->createMock(Trigger::class); $triggerMock->expects($this->exactly(3)) ->method('setName')->willReturnSelf(); - $triggerMock->expects($this->exactly(3)) + $triggerMock->expects($this->any()) ->method('getName') ->willReturn('triggerName'); $triggerMock->expects($this->exactly(3)) @@ -271,7 +303,7 @@ public function testRemove() true, [] ); - $otherChangelogMock->expects($this->exactly(3)) + $otherChangelogMock->expects($this->any()) ->method('getName') ->willReturn('other_test_view_cl'); $otherChangelogMock->expects($this->exactly(3)) @@ -297,7 +329,7 @@ public function testRemove() ->method('getChangelog') ->willReturn($otherChangelogMock); - $this->viewMock->expects($this->exactly(3)) + $this->viewMock->expects($this->any()) ->method('getId') ->willReturn('this_id'); $this->viewMock->expects($this->never()) @@ -343,12 +375,24 @@ public function testBuildStatementIgnoredColumnSubscriptionLevel(): void ] ] ]; + $mviewConfigMock = $this->createMock(Config::class); + $mviewConfigMock->expects($this->any()) + ->method('getView') + ->willReturn([ + 'subscriptions' => [ + $tableName => [ + 'processor' => DefaultProcessor::class + ] + ] + ]); - $this->connectionMock->expects($this->once()) + $this->connectionMock->expects($this->any()) ->method('isTableExists') + ->with('cataloginventory_stock_item') ->willReturn(true); - $this->connectionMock->expects($this->once()) + $this->connectionMock->expects($this->any()) ->method('describeTable') + ->with($tableName) ->willReturn([ 'item_id' => ['COLUMN_NAME' => 'item_id'], 'product_id' => ['COLUMN_NAME' => 'product_id'], @@ -359,10 +403,14 @@ public function testBuildStatementIgnoredColumnSubscriptionLevel(): void ]); $otherChangelogMock = $this->getMockForAbstractClass(ChangelogInterface::class); - $otherChangelogMock->expects($this->once()) + $otherChangelogMock->expects($this->any()) ->method('getViewId') ->willReturn($viewId); + $otherChangelogMock->expects($this->once()) + ->method('getColumnName') + ->willReturn('entity_id'); + $model = new Subscription( $this->resourceMock, $this->triggerFactoryMock, @@ -371,7 +419,8 @@ public function testBuildStatementIgnoredColumnSubscriptionLevel(): void $tableName, 'columnName', [], - $ignoredData + $ignoredData, + $mviewConfigMock ); $method = new \ReflectionMethod($model, 'buildStatement'); diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/ViewTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/ViewTest.php index 7d69ff43f158b..8d460ac99a46b 100644 --- a/lib/internal/Magento/Framework/Mview/Test/Unit/ViewTest.php +++ b/lib/internal/Magento/Framework/Mview/Test/Unit/ViewTest.php @@ -7,6 +7,7 @@ namespace Magento\Framework\Mview\Test\Unit; +use Laminas\Log\Filter\Mock; use Magento\Framework\Mview\ActionFactory; use Magento\Framework\Mview\ActionInterface; use Magento\Framework\Mview\ConfigInterface; @@ -53,6 +54,11 @@ class ViewTest extends TestCase */ protected $subscriptionFactoryMock; + /** + * @var MockObject|View\ChangeLogBatchIteratorInterface + */ + private $iteratorMock; + /** * @inheritdoc */ @@ -67,6 +73,7 @@ protected function setUp(): void true, ['getView'] ); + $this->iteratorMock = $this->createMock(View\ChangeLogBatchIteratorInterface::class); $this->actionFactoryMock = $this->createPartialMock(ActionFactory::class, ['get']); $this->stateMock = $this->createPartialMock( State::class, @@ -97,7 +104,10 @@ protected function setUp(): void $this->actionFactoryMock, $this->stateMock, $this->changelogMock, - $this->subscriptionFactoryMock + $this->subscriptionFactoryMock, + [], + [], + $this->iteratorMock ); } @@ -334,7 +344,7 @@ public function testUpdate() $currentVersionId ); $this->changelogMock->expects( - $this->once() + $this->any() )->method( 'getList' )->with( @@ -345,6 +355,7 @@ public function testUpdate() ); $actionMock = $this->getMockForAbstractClass(ActionInterface::class); + $this->iteratorMock->expects($this->once())->method('walk')->willReturn($listId); $actionMock->expects($this->once())->method('execute')->with($listId)->willReturnSelf(); $this->actionFactoryMock->expects( $this->once() @@ -390,7 +401,7 @@ public function testUpdateEx(): void ->expects($this->once()) ->method('getVersion') ->willReturn($currentVersionId); - + $this->iteratorMock->expects($this->any())->method('walk')->willReturn($this->generateChangeLog(150, 1, 150)); $this->changelogMock->method('getList') ->willReturnMap( [ @@ -401,7 +412,7 @@ public function testUpdateEx(): void ); $actionMock = $this->getMockForAbstractClass(ActionInterface::class); - $actionMock->expects($this->once()) + $actionMock->expects($this->any()) ->method('execute') ->with($this->generateChangeLog(150, 1, 150)) ->willReturnSelf(); @@ -457,7 +468,7 @@ public function testUpdateWithException() $this->stateMock->expects($this->atLeastOnce()) ->method('getMode') ->willReturn(StateInterface::MODE_ENABLED); - $this->stateMock->expects($this->exactly(2)) + $this->stateMock->expects($this->any()) ->method('getStatus') ->willReturn(StateInterface::STATUS_IDLE); $this->stateMock->expects($this->exactly(2)) @@ -472,16 +483,9 @@ public function testUpdateWithException() )->willReturn( $currentVersionId ); - $this->changelogMock->expects( - $this->once() - )->method( - 'getList' - )->with( - $lastVersionId, - $currentVersionId - )->willReturn( - $listId - ); + $this->iteratorMock->expects($this->any()) + ->method('walk') + ->willReturn([2,3]); $actionMock = $this->createPartialMock(ActionInterface::class, ['execute']); $actionMock->expects($this->once())->method('execute')->with($listId)->willReturnCallback( @@ -767,8 +771,11 @@ public function testGetUpdated() protected function loadView() { $viewId = 'view_test'; + $this->changelogMock->expects($this->any()) + ->method('getViewId') + ->willReturn($viewId); $this->configMock->expects( - $this->once() + $this->any() )->method( 'getView' )->with( @@ -788,7 +795,7 @@ protected function getViewData() 'view_id' => 'view_test', 'action_class' => 'Some\Class\Name', 'group' => 'some_group', - 'subscriptions' => ['some_entity' => ['name' => 'some_entity', 'column' => 'entity_id']] + 'subscriptions' => ['some_entity' => ['name' => 'some_entity', 'column' => 'entity_id']], ]; } } diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/_files/mview_config.php b/lib/internal/Magento/Framework/Mview/Test/Unit/_files/mview_config.php index a19f90546bac3..af488ecf5589e 100644 --- a/lib/internal/Magento/Framework/Mview/Test/Unit/_files/mview_config.php +++ b/lib/internal/Magento/Framework/Mview/Test/Unit/_files/mview_config.php @@ -20,14 +20,19 @@ 'some_entity' => [ 'name' => 'some_entity', 'column' => 'entity_id', - 'subscription_model' => null + 'subscription_model' => null, + 'additional_columns' => [], + 'processor' => \Magento\Framework\Mview\View\AdditionalColumnsProcessor\DefaultProcessor::class ], 'some_product_relation' => [ 'name' => 'some_product_relation', 'column' => 'product_id', - 'subscription_model' => null + 'subscription_model' => null, + 'additional_columns' => [], + 'processor' => \Magento\Framework\Mview\View\AdditionalColumnsProcessor\DefaultProcessor::class ], ], + 'walker' => \Magento\Framework\Mview\View\ChangeLogBatchWalker::class ], ] ]; diff --git a/lib/internal/Magento/Framework/Mview/View.php b/lib/internal/Magento/Framework/Mview/View.php index dade475a20482..420702c434103 100644 --- a/lib/internal/Magento/Framework/Mview/View.php +++ b/lib/internal/Magento/Framework/Mview/View.php @@ -9,7 +9,10 @@ namespace Magento\Framework\Mview; use InvalidArgumentException; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; +use Magento\Framework\Mview\View\ChangeLogBatchWalkerFactory; +use Magento\Framework\Mview\View\ChangeLogBatchWalkerInterface; use Magento\Framework\Mview\View\ChangelogTableNotExistsException; use Magento\Framework\Mview\View\SubscriptionFactory; use Exception; @@ -27,11 +30,6 @@ class View extends DataObject implements ViewInterface */ const DEFAULT_BATCH_SIZE = 1000; - /** - * Max versions to load from database at a time - */ - private static $maxVersionQueryBatch = 100000; - /** * @var string */ @@ -67,6 +65,11 @@ class View extends DataObject implements ViewInterface */ private $changelogBatchSize; + /** + * @var ChangeLogBatchWalkerFactory + */ + private $changeLogBatchWalkerFactory; + /** * @param ConfigInterface $config * @param ActionFactory $actionFactory @@ -75,6 +78,7 @@ class View extends DataObject implements ViewInterface * @param SubscriptionFactory $subscriptionFactory * @param array $data * @param array $changelogBatchSize + * @param ChangeLogBatchWalkerFactory $changeLogBatchWalkerFactory */ public function __construct( ConfigInterface $config, @@ -83,7 +87,8 @@ public function __construct( View\ChangelogInterface $changelog, SubscriptionFactory $subscriptionFactory, array $data = [], - array $changelogBatchSize = [] + array $changelogBatchSize = [], + ChangeLogBatchWalkerFactory $changeLogBatchWalkerFactory = null ) { $this->config = $config; $this->actionFactory = $actionFactory; @@ -92,6 +97,8 @@ public function __construct( $this->subscriptionFactory = $subscriptionFactory; $this->changelogBatchSize = $changelogBatchSize; parent::__construct($data); + $this->changeLogBatchWalkerFactory = $changeLogBatchWalkerFactory ?: + ObjectManager::getInstance()->get(ChangeLogBatchWalkerFactory::class); } /** @@ -297,40 +304,28 @@ private function executeAction(ActionInterface $action, int $lastVersionId, int $vsFrom = $lastVersionId; while ($vsFrom < $currentVersionId) { - $ids = $this->getBatchOfIds($vsFrom, $currentVersionId); - // We run the actual indexer in batches. - // Chunked AFTER loading to avoid duplicates in separate chunks. - $chunks = array_chunk($ids, $batchSize); - foreach ($chunks as $ids) { - $action->execute($ids); + $walker = $this->getWalker(); + $ids = $walker->walk($this->getChangelog(), $vsFrom, $currentVersionId, $batchSize); + + if (empty($ids)) { + break; } + $vsFrom += $batchSize; + $action->execute($ids); } } /** - * Get batch of entity ids + * Create and validate walker class for changelog * - * @param int $lastVersionId - * @param int $currentVersionId - * @return array + * @return ChangeLogBatchWalkerInterface|mixed + * @throws Exception */ - private function getBatchOfIds(int &$lastVersionId, int $currentVersionId): array + private function getWalker(): ChangeLogBatchWalkerInterface { - $ids = []; - $versionBatchSize = self::$maxVersionQueryBatch; - $idsBatchSize = self::$maxVersionQueryBatch; - for ($vsFrom = $lastVersionId; $vsFrom < $currentVersionId; $vsFrom += $versionBatchSize) { - // Don't go past the current version for atomicity. - $versionTo = min($currentVersionId, $vsFrom + $versionBatchSize); - /** To avoid duplicate ids need to flip and merge the array */ - $ids += array_flip($this->getChangelog()->getList($vsFrom, $versionTo)); - $lastVersionId = $versionTo; - if (count($ids) >= $idsBatchSize) { - break; - } - } - - return array_keys($ids); + $config = $this->config->getView($this->changelog->getViewId()); + $walkerClass = $config['walker']; + return $this->changeLogBatchWalkerFactory->create($walkerClass); } /** diff --git a/lib/internal/Magento/Framework/Mview/View/AdditionalColumnProcessorInterface.php b/lib/internal/Magento/Framework/Mview/View/AdditionalColumnProcessorInterface.php new file mode 100644 index 0000000000000..c89fbce074a13 --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/View/AdditionalColumnProcessorInterface.php @@ -0,0 +1,38 @@ +resourceConnection = $resourceConnection; + } + + /** + * @inheritDoc + */ + public function getTriggerColumns(string $eventPrefix, array $additionalColumns): array + { + $resource = $this->resourceConnection->getConnection(); + $triggersColumns = [ + 'column_names' => [], + 'column_values' => [] + ]; + + foreach ($additionalColumns as $additionalColumn) { + $triggersColumns['column_names'][$additionalColumn['name']] = $resource->quoteIdentifier( + $additionalColumn['cl_name'] + ); + + $triggersColumns['column_values'][$additionalColumn['name']] = isset($additionalColumn['constant']) ? + $resource->quote($additionalColumn['constant']) : + $eventPrefix . $resource->quoteIdentifier($additionalColumn['name']); + } + + return $triggersColumns; + } + + /** + * @return string + */ + public function getPreStatements(): string + { + return ''; + } + + /** + * @inheritDoc + */ + public function processColumnForCLTable(Table $table, string $columnName): void + { + $table->addColumn( + $columnName, + \Magento\Framework\DB\Ddl\Table::TYPE_TEXT, + 255, + ['unsigned' => true, 'nullable' => true, 'default' => null], + $columnName + ); + } +} diff --git a/lib/internal/Magento/Framework/Mview/View/AdditionalColumnsProcessor/ProcessorFactory.php b/lib/internal/Magento/Framework/Mview/View/AdditionalColumnsProcessor/ProcessorFactory.php new file mode 100644 index 0000000000000..5907cefbffd50 --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/View/AdditionalColumnsProcessor/ProcessorFactory.php @@ -0,0 +1,38 @@ +objectManager = $objectManager; + } + + /** + * Instantiate additional columns processor + * + * @param string $processorClassName + * @return AdditionalColumnProcessorInterface + */ + public function create(string $processorClassName): AdditionalColumnProcessorInterface + { + return $this->objectManager->create($processorClassName); + } +} diff --git a/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalker.php b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalker.php new file mode 100644 index 0000000000000..7a767e656c3ca --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalker.php @@ -0,0 +1,59 @@ +resourceConnection = $resourceConnection; + } + + /** + * @inheritdoc + */ + public function walk(ChangelogInterface $changelog, int $fromVersionId, int $toVersion, int $batchSize) + { + $connection = $this->resourceConnection->getConnection(); + $changelogTableName = $this->resourceConnection->getTableName($changelog->getName()); + + if (!$connection->isTableExists($changelogTableName)) { + throw new ChangelogTableNotExistsException(new Phrase("Table %1 does not exist", [$changelogTableName])); + } + + $select = $connection->select()->distinct(true) + ->where( + 'version_id > ?', + $fromVersionId + ) + ->where( + 'version_id <= ?', + $toVersion + ) + ->group([$changelog->getColumnName()]) + ->limit($batchSize); + + $select->from($changelogTableName, [$changelog->getColumnName()]); + return $connection->fetchCol($select); + } +} diff --git a/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerFactory.php b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerFactory.php new file mode 100644 index 0000000000000..98d814775f62b --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerFactory.php @@ -0,0 +1,37 @@ +objectManager = $objectManager; + } + + /** + * Instantiate BatchWalker interface + * + * @param string $batchWalkerClassName + * @return ChangeLogBatchWalkerInterface + */ + public function create(string $batchWalkerClassName): ChangeLogBatchWalkerInterface + { + return $this->objectManager->create($batchWalkerClassName); + } +} diff --git a/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerInterface.php b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerInterface.php new file mode 100644 index 0000000000000..d9079c550403c --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/View/ChangeLogBatchWalkerInterface.php @@ -0,0 +1,30 @@ +connection = $resource->getConnection(); $this->resource = $resource; $this->checkConnection(); + $this->mviewConfig = $mviewConfig; + $this->additionalColumnsProcessorFactory = $additionalColumnsProcessorFactory; } /** @@ -83,7 +109,7 @@ public function create() $table = $this->connection->newTable( $changelogTableName )->addColumn( - 'version_id', + self::VERSION_ID_COLUMN_NAME, \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, null, ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], @@ -95,10 +121,46 @@ public function create() ['unsigned' => true, 'nullable' => false, 'default' => '0'], 'Entity ID' ); + + foreach ($this->initAdditionalColumnData() as $columnData) { + /** @var AdditionalColumnProcessorInterface $processor */ + $processorClass = $columnData['processor']; + $processor = $this->additionalColumnsProcessorFactory->create($processorClass); + $processor->processColumnForCLTable($table, $columnData['cl_name']); + } + $this->connection->createTable($table); } } + /** + * Retrieve additional column data + * + * @return array + * @throws \Exception + */ + private function initAdditionalColumnData(): array + { + $config = $this->mviewConfig->getView($this->getViewId()); + $additionalColumns = []; + + if (!$config) { + return $additionalColumns; + } + + foreach ($config['subscriptions'] as $subscription) { + if (isset($subscription['additional_columns'])) { + foreach ($subscription['additional_columns'] as $additionalColumn) { + //We are gatherig unique change log column names in order to create them later + $additionalColumns[$additionalColumn['cl_name']] = $additionalColumn; + $additionalColumns[$additionalColumn['cl_name']]['processor'] = $subscription['processor']; + } + } + } + + return $additionalColumns; + } + /** * Drop changelog table * @@ -139,7 +201,7 @@ public function clear($versionId) * * @param int $fromVersionId * @param int $toVersionId - * @return int[] + * @return array * @throws ChangelogTableNotExistsException */ public function getList($fromVersionId, $toVersionId) diff --git a/lib/internal/Magento/Framework/Mview/View/Subscription.php b/lib/internal/Magento/Framework/Mview/View/Subscription.php index 510a4579d160b..17e4dede4cbf8 100644 --- a/lib/internal/Magento/Framework/Mview/View/Subscription.php +++ b/lib/internal/Magento/Framework/Mview/View/Subscription.php @@ -6,8 +6,10 @@ namespace Magento\Framework\Mview\View; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Ddl\Trigger; +use Magento\Framework\Mview\Config; use Magento\Framework\Mview\View\StateInterface; /** @@ -72,6 +74,10 @@ class Subscription implements SubscriptionInterface * @var Resource */ protected $resource; + /** + * @var Config + */ + private $mviewConfig; /** * @param ResourceConnection $resource @@ -82,6 +88,7 @@ class Subscription implements SubscriptionInterface * @param string $columnName * @param array $ignoredUpdateColumns * @param array $ignoredUpdateColumnsBySubscription + * @param Config|null $mviewConfig */ public function __construct( ResourceConnection $resource, @@ -91,7 +98,8 @@ public function __construct( $tableName, $columnName, $ignoredUpdateColumns = [], - $ignoredUpdateColumnsBySubscription = [] + $ignoredUpdateColumnsBySubscription = [], + Config $mviewConfig = null ) { $this->connection = $resource->getConnection(); $this->triggerFactory = $triggerFactory; @@ -102,6 +110,7 @@ public function __construct( $this->resource = $resource; $this->ignoredUpdateColumns = $ignoredUpdateColumns; $this->ignoredUpdateColumnsBySubscription = $ignoredUpdateColumnsBySubscription; + $this->mviewConfig = $mviewConfig ?? ObjectManager::getInstance()->get(Config::class); } /** @@ -196,6 +205,38 @@ protected function getLinkedViews() return $this->linkedViews; } + /** + * Prepare columns for trigger statement. Should be protected in order to serve new approach + * + * @param ChangelogInterface $changelog + * @param string $event + * @return array + * @throws \Exception + */ + protected function prepareColumns(ChangelogInterface $changelog, string $event): array + { + $prefix = $event === Trigger::EVENT_DELETE ? 'OLD.' : 'NEW.'; + $subscriptionData = $this->mviewConfig->getView($changelog->getViewId())['subscriptions'][$this->getTableName()]; + $columns = [ + 'column_names' => [ + 'entity_id' => $this->connection->quoteIdentifier($changelog->getColumnName()) + ], + 'column_values' => [ + 'entity_id' => $this->getEntityColumn($prefix) + ] + ]; + + if (!empty($subscriptionData['additional_columns'])) { + $processor = $this->getProcessor(); + $columns = array_replace_recursive( + $columns, + $processor->getTriggerColumns($prefix, $subscriptionData['additional_columns']) + ); + } + + return $columns; + } + /** * Build trigger statement for INSERT, UPDATE, DELETE events * @@ -205,13 +246,10 @@ protected function getLinkedViews() */ protected function buildStatement($event, $changelog) { + $trigger = "%sINSERT IGNORE INTO %s (%s) VALUES (%s);"; switch ($event) { - case Trigger::EVENT_INSERT: - $trigger = "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);"; - break; case Trigger::EVENT_UPDATE: $tableName = $this->resource->getTableName($this->getTableName()); - $trigger = "INSERT IGNORE INTO %s (%s) VALUES (NEW.%s);"; if ($this->connection->isTableExists($tableName) && $describe = $this->connection->describeTable($tableName) ) { @@ -240,20 +278,47 @@ protected function buildStatement($event, $changelog) } } break; - case Trigger::EVENT_DELETE: - $trigger = "INSERT IGNORE INTO %s (%s) VALUES (OLD.%s);"; - break; - default: - return ''; } + $columns = $this->prepareColumns($changelog, $event); return sprintf( $trigger, + $this->getProcessor()->getPreStatements(), $this->connection->quoteIdentifier($this->resource->getTableName($changelog->getName())), - $this->connection->quoteIdentifier($changelog->getColumnName()), - $this->connection->quoteIdentifier($this->getColumnName()) + implode(", " , $columns['column_names']), + implode(", ", $columns['column_values']) ); } + /** + * Instantiate and retrieve additional columns processor + * + * @return AdditionalColumnProcessorInterface + * @throws \Exception + */ + private function getProcessor(): AdditionalColumnProcessorInterface + { + $subscriptionData = $this->mviewConfig->getView($this->getView()->getId())['subscriptions']; + $processorClass = $subscriptionData[$this->getTableName()]['processor']; + $processor = ObjectManager::getInstance()->get($processorClass); + + if (!$processor instanceof AdditionalColumnProcessorInterface) { + throw new \Exception( + 'Processor should implements ' . AdditionalColumnProcessorInterface::class + ); + } + + return $processor; + } + + /** + * @param string $prefix + * @return string + */ + public function getEntityColumn(string $prefix): string + { + return $prefix . $this->connection->quoteIdentifier($this->getColumnName()); + } + /** * Build an "after" event for the given table and event * diff --git a/lib/internal/Magento/Framework/Mview/etc/mview.xsd b/lib/internal/Magento/Framework/Mview/etc/mview.xsd index dfff4964f6587..04754fa499249 100644 --- a/lib/internal/Magento/Framework/Mview/etc/mview.xsd +++ b/lib/internal/Magento/Framework/Mview/etc/mview.xsd @@ -46,6 +46,7 @@ + @@ -76,9 +77,25 @@ Table declaration. + + + + + + + + + + + + + + + + diff --git a/lib/internal/Magento/Framework/View/Helper/SecureHtmlRenderer.php b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRenderer.php index ae8ab3f15bc96..052c51441d451 100644 --- a/lib/internal/Magento/Framework/View/Helper/SecureHtmlRenderer.php +++ b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRenderer.php @@ -8,7 +8,6 @@ namespace Magento\Framework\View\Helper; -use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\Math\Random; use Magento\Framework\View\Helper\SecureHtmlRender\EventHandlerData; use Magento\Framework\View\Helper\SecureHtmlRender\HtmlRenderer; @@ -105,23 +104,27 @@ public function renderEventListenerAsTag( } $random = $this->random->getRandomString(10); - $listenerFunction = 'eventListener' .$random; - $elementName = 'listenedElement' .$random; + $listenerFunction = 'eventListener' . $random; + $elementName = 'listenedElement' . $random; $script = <<