diff --git a/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php b/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php index 5a09e1f17f617..6004d08b4b738 100644 --- a/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php +++ b/app/code/Magento/Backend/Block/System/Design/Edit/Tab/General.php @@ -6,6 +6,9 @@ namespace Magento\Backend\Block\System\Design\Edit\Tab; +/** + * General system tab block. + */ class General extends \Magento\Backend\Block\Widget\Form\Generic { /** @@ -90,7 +93,7 @@ protected function _prepareForm() ] ); - $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); + $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); $fieldset->addField( 'date_from', 'date', diff --git a/app/code/Magento/Backend/Model/Session/Quote.php b/app/code/Magento/Backend/Model/Session/Quote.php index 11edaa26f443f..e32f1bc57596e 100644 --- a/app/code/Magento/Backend/Model/Session/Quote.php +++ b/app/code/Magento/Backend/Model/Session/Quote.php @@ -149,7 +149,8 @@ public function getQuote() $this->_quote = $this->quoteFactory->create(); if ($this->getStoreId()) { if (!$this->getQuoteId()) { - $this->_quote->setCustomerGroupId($this->groupManagement->getDefaultGroup()->getId()); + $customerGroupId = $this->groupManagement->getDefaultGroup($this->getStoreId())->getId(); + $this->_quote->setCustomerGroupId($customerGroupId); $this->_quote->setIsActive(false); $this->_quote->setStoreId($this->getStoreId()); diff --git a/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php b/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php index 869d4ba3f45b1..d159225089afc 100644 --- a/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Session/QuoteTest.php @@ -267,7 +267,10 @@ public function testGetQuoteWithoutQuoteId() $cartInterfaceMock->expects($this->atLeastOnce())->method('getId')->willReturn($quoteId); $defaultGroup = $this->getMockBuilder(\Magento\Customer\Api\Data\GroupInterface::class)->getMock(); $defaultGroup->expects($this->any())->method('getId')->will($this->returnValue($customerGroupId)); - $this->groupManagementMock->expects($this->any())->method('getDefaultGroup')->willReturn($defaultGroup); + $this->groupManagementMock + ->method('getDefaultGroup') + ->with($storeId) + ->willReturn($defaultGroup); $dataCustomerMock = $this->getMockBuilder(\Magento\Customer\Api\Data\CustomerInterface::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index e3411166ee4a4..44ab1dcc00176 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -153,15 +153,15 @@ - + Magento\Config\Model\Config\Source\Yesno - + Magento\Config\Model\Config\Source\Yesno - + Magento\Config\Model\Config\Source\Yesno Minification is not applied in developer mode. @@ -169,11 +169,11 @@ - + Magento\Config\Model\Config\Source\Yesno - + Magento\Config\Model\Config\Source\Yesno Minification is not applied in developer mode. diff --git a/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontBraintreeFillCardDataActionGroup.xml b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontBraintreeFillCardDataActionGroup.xml new file mode 100644 index 0000000000000..93f239b5c38a1 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/ActionGroup/StorefrontBraintreeFillCardDataActionGroup.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml b/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml index 0c57147ca950e..4e8526a2b0e2b 100644 --- a/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml +++ b/app/code/Magento/Braintree/Test/Mftf/Data/BraintreeData.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> SampleTitle SamplePaymentAction @@ -113,5 +113,4 @@ 20 113 - diff --git a/app/code/Magento/Braintree/Test/Mftf/Page/CheckoutPage.xml b/app/code/Magento/Braintree/Test/Mftf/Page/CheckoutPage.xml new file mode 100644 index 0000000000000..bc23a1e1fe3f7 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Page/CheckoutPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontBraintreePaymentConfigurationSection.xml b/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontBraintreePaymentConfigurationSection.xml new file mode 100644 index 0000000000000..f72eed9179764 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Mftf/Section/StorefrontBraintreePaymentConfigurationSection.xml @@ -0,0 +1,22 @@ + + + + +
+ + + + + + + + + +
+
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php index 80d333db80f0a..6925e37b580ac 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Request/VaultCaptureDataBuilderTest.php @@ -111,7 +111,7 @@ public function testBuild() * @expectedException \Magento\Payment\Gateway\Command\CommandException * @expectedExceptionMessage The Payment Token is not available to perform the request. */ - public function testBuildWithoutPaymentToken(): void + public function testBuildWithoutPaymentToken() { $amount = 30.00; $buildSubject = [ diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php index 09fce9222e63d..dbd07f188f90b 100644 --- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php +++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php @@ -7,6 +7,7 @@ use Magento\Bundle\Api\ProductOptionRepositoryInterface as OptionRepository; use Magento\Bundle\Api\ProductLinkManagementInterface; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\ExtensionInterface; @@ -49,12 +50,11 @@ public function __construct( } /** + * Perform action on Bundle product relation/extension attribute. + * * @param object $entity * @param array $arguments - * @return \Magento\Catalog\Api\Data\ProductInterface|object - * @throws \Magento\Framework\Exception\NoSuchEntityException - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\CouldNotSaveException + * @return ProductInterface|object * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function execute($entity, $arguments = []) @@ -78,7 +78,7 @@ public function execute($entity, $arguments = []) $options = $bundleProductOptions ?: []; if (!$entity->getCopyFromView()) { - $this->processRemovedOptions($entity->getSku(), $existingOptionsIds, $optionIds); + $this->processRemovedOptions($entity, $existingOptionsIds, $optionIds); $newOptionsIds = array_diff($optionIds, $existingOptionsIds); $this->saveOptions($entity, $options, $newOptionsIds); @@ -92,10 +92,10 @@ public function execute($entity, $arguments = []) } /** + * Remove option product links. + * * @param string $entitySku * @param \Magento\Bundle\Api\Data\OptionInterface $option - * @throws \Magento\Framework\Exception\NoSuchEntityException - * @throws \Magento\Framework\Exception\InputException * @return void */ protected function removeOptionLinks($entitySku, $option) @@ -152,21 +152,21 @@ private function getOptionIds(array $options) } /** - * Removes old options that no longer exists + * Removes old options that no longer exists. * - * @param string $entitySku + * @param ProductInterface $entity * @param array $existingOptionsIds * @param array $optionIds - * @throws \Magento\Framework\Exception\NoSuchEntityException - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\CouldNotSaveException * @return void */ - private function processRemovedOptions($entitySku, array $existingOptionsIds, array $optionIds) + private function processRemovedOptions(ProductInterface $entity, array $existingOptionsIds, array $optionIds) { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $parentId = $entity->getData($metadata->getLinkField()); foreach (array_diff($existingOptionsIds, $optionIds) as $optionId) { - $option = $this->optionRepository->get($entitySku, $optionId); - $this->removeOptionLinks($entitySku, $option); + $option = $this->optionRepository->get($entity->getSku(), $optionId); + $option->setParentId($parentId); + $this->removeOptionLinks($entity->getSku(), $option); $this->optionRepository->delete($option); } } diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php new file mode 100644 index 0000000000000..41e6c2c160e0a --- /dev/null +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php @@ -0,0 +1,279 @@ +objectManager = new ObjectManager($this); + + $this->productMock = $this->getMockBuilder(ProductInterface::class) + ->setMethods( + [ + 'getExtensionAttributes', + 'getCopyFromView', + 'getData', + 'getTypeId', + 'getSku', + ] + ) + ->getMockForAbstractClass(); + $this->productExtensionMock = $this->getMockBuilder(ProductExtensionInterface::class) + ->setMethods(['getBundleProductOptions']) + ->getMockForAbstractClass(); + $this->optionMock = $this->getMockBuilder(OptionInterface::class) + ->setMethods( + [ + 'setParentId', + 'getId', + 'getOptionId', + ] + ) + ->getMockForAbstractClass(); + $this->optionRepositoryMock = $this->createMock(ProductOptionRepositoryInterface::class); + $this->productLinkManagementMock = $this->createMock(ProductLinkManagementInterface::class); + $this->linkMock = $this->createMock(LinkInterface::class); + $this->metadataPoolMock = $this->createMock(MetadataPool::class); + $this->metadataMock = $this->createMock(EntityMetadataInterface::class); + $this->metadataPoolMock->expects($this->any()) + ->method('getMetadata') + ->willReturn($this->metadataMock); + + $this->saveHandler = $this->objectManager->getObject( + SaveHandler::class, + [ + 'optionRepository' => $this->optionRepositoryMock, + 'productLinkManagement' => $this->productLinkManagementMock, + 'metadataPool' => $this->metadataPoolMock, + ] + ); + } + + /** + * @return void + */ + public function testExecuteWithInvalidProductType() + { + $productType = 'simple'; + + $this->productMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->productExtensionMock->expects($this->once()) + ->method('getBundleProductOptions') + ->willReturn([]); + $this->productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn($productType); + + $entity = $this->saveHandler->execute($this->productMock); + $this->assertSame($this->productMock, $entity); + } + + /** + * @return void + */ + public function testExecuteWithoutExistingOption() + { + $productType = 'bundle'; + $productSku = 'product-sku'; + $optionId = null; + + $this->productMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->productExtensionMock->expects($this->once()) + ->method('getBundleProductOptions') + ->willReturn([$this->optionMock]); + + $this->productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn($productType); + + $this->productMock->expects($this->once()) + ->method('getSku') + ->willReturn($productSku); + $this->optionRepositoryMock->expects($this->once()) + ->method('getList') + ->with($productSku) + ->willReturn([]); + + $this->optionMock->expects($this->any()) + ->method('getOptionId') + ->willReturn($optionId); + + $this->productMock->expects($this->once()) + ->method('getCopyFromView') + ->willReturn(false); + + $this->optionMock->expects($this->never())->method('setOptionId'); + $this->optionRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->productMock, $this->optionMock) + ->willReturn($optionId); + + $this->saveHandler->execute($this->productMock); + } + + /** + * @return void + */ + public function testExecuteWithExistingOption() + { + $productType = 'bundle'; + $productSku = 'product-sku'; + $productLinkSku = 'product-link-sku'; + $linkField = 'entity_id'; + $parentId = 1; + $existingOptionId = 1; + $optionId = 2; + + /** @var OptionInterface|MockObject $existingOptionMock */ + $existingOptionMock = $this->getMockBuilder(OptionInterface::class) + ->setMethods(['getOptionId']) + ->getMockForAbstractClass(); + + $this->productMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->productExtensionMock->expects($this->once()) + ->method('getBundleProductOptions') + ->willReturn([$this->optionMock]); + $this->productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn($productType); + + $this->productMock->expects($this->exactly(3)) + ->method('getSku') + ->willReturn($productSku); + $this->optionRepositoryMock->expects($this->once()) + ->method('getList') + ->with($productSku) + ->willReturn([$existingOptionMock]); + + $existingOptionMock->expects($this->any()) + ->method('getOptionId') + ->willReturn($existingOptionId); + $this->optionMock->expects($this->any()) + ->method('getOptionId') + ->willReturn($optionId); + + $this->productMock->expects($this->once()) + ->method('getCopyFromView') + ->willReturn(false); + $this->metadataMock->expects($this->once()) + ->method('getLinkField') + ->willReturn($linkField); + $this->productMock->expects($this->once()) + ->method('getData') + ->with($linkField) + ->willReturn($parentId); + + $this->optionRepositoryMock->expects($this->once()) + ->method('get') + ->with($productSku, $existingOptionId) + ->willReturn($this->optionMock); + $this->optionMock->expects($this->once()) + ->method('setParentId') + ->with($parentId) + ->willReturnSelf(); + $this->optionMock->expects($this->once()) + ->method('getProductLinks') + ->willReturn([$this->linkMock]); + $this->linkMock->expects($this->once()) + ->method('getSku') + ->willReturn($productLinkSku); + + $this->optionMock->expects($this->any()) + ->method('getId') + ->willReturn($existingOptionId); + $this->productLinkManagementMock->expects($this->once()) + ->method('removeChild') + ->with($productSku, $existingOptionId, $productLinkSku) + ->willReturn(true); + $this->optionRepositoryMock->expects($this->once()) + ->method('delete') + ->with($this->optionMock) + ->willReturn(true); + + $this->optionRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->productMock, $this->optionMock) + ->willReturn($optionId); + + $this->saveHandler->execute($this->productMock); + } +} diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index ed615b41644e2..a7bb242daf86f 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -73,7 +73,7 @@ public function __construct( } /** - * @return void + * @inheritdoc */ protected function _construct() { @@ -82,7 +82,7 @@ protected function _construct() } /** - * @return $this + * @inheritdoc */ protected function _prepareLayout() { @@ -182,6 +182,8 @@ public function getSuggestedCategoriesJson($namePart) } /** + * Get add root button html + * * @return string */ public function getAddRootButtonHtml() @@ -190,6 +192,8 @@ public function getAddRootButtonHtml() } /** + * Get add sub button html + * * @return string */ public function getAddSubButtonHtml() @@ -198,6 +202,8 @@ public function getAddSubButtonHtml() } /** + * Get expand button html + * * @return string */ public function getExpandButtonHtml() @@ -206,6 +212,8 @@ public function getExpandButtonHtml() } /** + * Get collapse button html + * * @return string */ public function getCollapseButtonHtml() @@ -214,6 +222,8 @@ public function getCollapseButtonHtml() } /** + * Get store switcher + * * @return string */ public function getStoreSwitcherHtml() @@ -222,6 +232,8 @@ public function getStoreSwitcherHtml() } /** + * Get loader tree url + * * @param bool|null $expanded * @return string */ @@ -235,6 +247,8 @@ public function getLoadTreeUrl($expanded = null) } /** + * Get nodes url + * * @return string */ public function getNodesUrl() @@ -243,6 +257,8 @@ public function getNodesUrl() } /** + * Get switcher tree url + * * @return string */ public function getSwitchTreeUrl() @@ -254,6 +270,8 @@ public function getSwitchTreeUrl() } /** + * Get is was expanded + * * @return bool * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ @@ -263,7 +281,10 @@ public function getIsWasExpanded() } /** + * Get move url + * * @return string + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getMoveUrl() { @@ -271,6 +292,8 @@ public function getMoveUrl() } /** + * Get tree + * * @param mixed|null $parenNodeCategory * @return array */ @@ -282,6 +305,8 @@ public function getTree($parenNodeCategory = null) } /** + * Get tree json + * * @param mixed|null $parenNodeCategory * @return string */ @@ -367,7 +392,7 @@ protected function _getNodeJson($node, $level = 0) } } - if ($isParent || $node->getLevel() < 2) { + if ($isParent || $node->getLevel() < 1) { $item['expanded'] = true; } @@ -390,6 +415,8 @@ public function buildNodeName($node) } /** + * Is category movable + * * @param Node|array $node * @return bool */ @@ -403,6 +430,8 @@ protected function _isCategoryMoveable($node) } /** + * Is parent selected category + * * @param Node|array $node * @return bool */ @@ -422,6 +451,7 @@ protected function _isParentSelectedCategory($node) * Check if page loaded by outside link to category edit * * @return boolean + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function isClearEdit() { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php index 4aebd521fe60d..964872b6e51bd 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Inventory.php @@ -70,11 +70,11 @@ public function getFieldSuffix() * Retrieve current store id * * @return int + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getStoreId() { - $storeId = $this->getRequest()->getParam('store'); - return (int)$storeId; + return (int)$this->getRequest()->getParam('store'); } /** @@ -99,6 +99,8 @@ public function getTabLabel() } /** + * Return Tab title. + * * @return \Magento\Framework\Phrase */ public function getTabTitle() @@ -107,7 +109,7 @@ public function getTabTitle() } /** - * @return bool + * @inheritdoc */ public function canShowTab() { @@ -115,7 +117,7 @@ public function canShowTab() } /** - * @return bool + * @inheritdoc */ public function isHidden() { @@ -123,6 +125,8 @@ public function isHidden() } /** + * Get availability status. + * * @param string $fieldName * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index b353e477a056c..69c8b78b017d2 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -16,6 +16,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; /** + * Attributes attributes block + * * @api * @since 100.0.2 */ @@ -56,6 +58,8 @@ public function __construct( } /** + * Returns a Product. + * * @return Product */ public function getProduct() @@ -67,6 +71,8 @@ public function getProduct() } /** + * Additional data. + * * $excludeAttr is optional array of attribute codes to * exclude them from additional data array * @@ -89,9 +95,9 @@ public function getAdditionalData(array $excludeAttr = []) $value = $this->priceCurrency->convertAndFormat($value); } - if (is_string($value) && strlen($value)) { + if (is_string($value) && strlen(trim($value))) { $data[$attribute->getAttributeCode()] = [ - 'label' => __($attribute->getStoreLabel()), + 'label' => $attribute->getStoreLabel(), 'value' => $value, 'code' => $attribute->getAttributeCode(), ]; diff --git a/app/code/Magento/Catalog/Block/Product/View/Options.php b/app/code/Magento/Catalog/Block/Product/View/Options.php index 0720c018f6a9b..c457b20cd0904 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options.php @@ -4,16 +4,15 @@ * See COPYING.txt for license details. */ -/** - * Product options block - * - * @author Magento Core Team - */ namespace Magento\Catalog\Block\Product\View; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option\Value; /** + * Product options block + * + * @author Magento Core Team * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -121,6 +120,8 @@ public function setProduct(Product $product = null) } /** + * Get group of option. + * * @param string $type * @return string */ @@ -142,6 +143,8 @@ public function getOptions() } /** + * Check if block has options. + * * @return bool */ public function hasOptions() @@ -160,7 +163,10 @@ public function hasOptions() */ protected function _getPriceConfiguration($option) { - $optionPrice = $this->pricingHelper->currency($option->getPrice(true), false, false); + $optionPrice = $option->getPrice(true); + if ($option->getPriceType() !== Value::TYPE_PERCENT) { + $optionPrice = $this->pricingHelper->currency($optionPrice, false, false); + } $data = [ 'prices' => [ 'oldPrice' => [ @@ -195,7 +201,7 @@ protected function _getPriceConfiguration($option) ], ], 'type' => $option->getPriceType(), - 'name' => $option->getTitle() + 'name' => $option->getTitle(), ]; return $data; } @@ -231,7 +237,7 @@ public function getJsonConfig() //pass the return array encapsulated in an object for the other modules to be able to alter it eg: weee $this->_eventManager->dispatch('catalog_product_option_price_configuration_after', ['configObj' => $configObj]); - $config=$configObj->getConfig(); + $config = $configObj->getConfig(); return $this->_jsonEncoder->encode($config); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index 3a81b4633b2ff..03d143fff036f 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -100,7 +100,7 @@ public function execute() $attributeCode ); - if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type') { + if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type' || $attributeCode === 'type_id') { $message = strlen($this->getRequest()->getParam('attribute_code')) ? __('An attribute with this code already exists.') : __('An attribute with the same code (%1) already exists.', $attributeCode); @@ -158,7 +158,7 @@ private function isUniqueAdminValues(array $optionsValues, array $deletedOptions { $adminValues = []; foreach ($optionsValues as $optionKey => $values) { - if (!(isset($deletedOptions[$optionKey]) and $deletedOptions[$optionKey] === '1')) { + if (!(isset($deletedOptions[$optionKey]) && $deletedOptions[$optionKey] === '1')) { $adminValues[] = reset($values); } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index 7153f9fd0f3f9..f11d16755ef0d 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -19,6 +19,8 @@ use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper\AttributeFilter; /** + * Product helper + * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -104,7 +106,7 @@ class Helper * @param \Magento\Backend\Helper\Js $jsHelper * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter * @param CustomOptionFactory|null $customOptionFactory - * @param ProductLinkFactory |null $productLinkFactory + * @param ProductLinkFactory|null $productLinkFactory * @param ProductRepositoryInterface|null $productRepository * @param LinkTypeProvider|null $linkTypeProvider * @param AttributeFilter|null $attributeFilter @@ -365,6 +367,8 @@ private function overwriteValue($optionId, $option, $overwriteOptions) } /** + * Get link resolver instance + * * @return LinkResolver * @deprecated 101.0.0 */ @@ -377,6 +381,8 @@ private function getLinkResolver() } /** + * Get DateTimeFilter instance + * * @return \Magento\Framework\Stdlib\DateTime\Filter\DateTime * @deprecated 101.0.0 */ @@ -391,6 +397,7 @@ private function getDateTimeFilter() /** * Remove ids of non selected websites from $websiteIds array and return filtered data + * * $websiteIds parameter expects array with website ids as keys and 1 (selected) or 0 (non selected) as values * Only one id (default website ID) will be set to $websiteIds array when the single store mode is turned on * @@ -463,6 +470,7 @@ private function fillProductOptions(Product $product, array $productOptions) private function convertSpecialFromDateStringToObject($productData) { if (isset($productData['special_from_date']) && $productData['special_from_date'] != '') { + $productData['special_from_date'] = $this->getDateTimeFilter()->filter($productData['special_from_date']); $productData['special_from_date'] = new \DateTime($productData['special_from_date']); } diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 00b093b2918f1..aa99918753e81 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -1130,10 +1130,15 @@ public function reindex() } } $productIndexer = $this->indexerRegistry->get(Indexer\Category\Product::INDEXER_ID); - if (!$productIndexer->isScheduled() - && (!empty($this->getAffectedProductIds()) || $this->dataHasChangedFor('is_anchor')) - ) { - $productIndexer->reindexList($this->getPathIds()); + + if (!empty($this->getAffectedProductIds()) + || $this->dataHasChangedFor('is_anchor') + || $this->dataHasChangedFor('is_active')) { + if (!$productIndexer->isScheduled()) { + $productIndexer->reindexList($this->getPathIds()); + } else { + $productIndexer->invalidate(); + } } } @@ -1165,16 +1170,14 @@ public function getIdentities() $identities[] = self::CACHE_TAG . '_' . $this->getId(); } - if ($this->hasDataChanges() || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU)) { - $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); - } - + $identities = $this->getCategoryRelationIdentities($identities); + if ($this->isObjectNew()) { $identities[] = self::CACHE_TAG; } } - return $identities; + return array_unique($identities); } /** @@ -1460,5 +1463,25 @@ public function setExtensionAttributes(\Magento\Catalog\Api\Data\CategoryExtensi return $this->_setExtensionAttributes($extensionAttributes); } + /** + * Return category relation identities. + * + * @param array $identities + * @return array + */ + private function getCategoryRelationIdentities(array $identities): array + { + if ($this->hasDataChanges() || $this->isDeleted() || $this->dataHasChangedFor(self::KEY_INCLUDE_IN_MENU)) { + $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $this->getId(); + if ($this->dataHasChangedFor('is_anchor') || $this->dataHasChangedFor('is_active')) { + foreach ($this->getPathIds() as $id) { + $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $id; + } + } + } + + return $identities; + } + //@codeCoverageIgnoreEnd } diff --git a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php index c4a2d60414a7b..d15e8ef5efa55 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Option/SaveHandler.php @@ -28,6 +28,8 @@ public function __construct( } /** + * Perform action on relation/extension attribute. + * * @param object $entity * @param array $arguments * @return \Magento\Catalog\Api\Data\ProductInterface|object @@ -35,6 +37,10 @@ public function __construct( */ public function execute($entity, $arguments = []) { + if ($entity->getOptionsSaved()) { + return $entity; + } + $options = $entity->getOptions(); $optionIds = []; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index 12009e62fd27e..6614b10ef609e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -9,6 +9,10 @@ namespace Magento\Catalog\Model\ResourceModel; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; +use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Catalog entity abstract model @@ -39,16 +43,18 @@ abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Factory $modelFactory * @param array $data + * @param UniqueValidationInterface|null $uniqueValidator */ public function __construct( \Magento\Eav\Model\Entity\Context $context, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Factory $modelFactory, - $data = [] + $data = [], + UniqueValidationInterface $uniqueValidator = null ) { $this->_storeManager = $storeManager; $this->_modelFactory = $modelFactory; - parent::__construct($context, $data); + parent::__construct($context, $data, $uniqueValidator); } /** @@ -88,14 +94,14 @@ protected function _isApplicableAttribute($object, $attribute) /** * Check whether attribute instance (attribute, backend, frontend or source) has method and applicable * - * @param AbstractAttribute|\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend|\Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend|\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource $instance + * @param AbstractAttribute|AbstractBackend|AbstractFrontend|AbstractSource $instance * @param string $method * @param array $args array of arguments * @return boolean */ protected function _isCallableAttributeInstance($instance, $method, $args) { - if ($instance instanceof \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend + if ($instance instanceof AbstractBackend && ($method == 'beforeSave' || $method == 'afterSave') ) { $attributeCode = $instance->getAttribute()->getAttributeCode(); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index b4b78996f762f..e438e2f54113d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -8,6 +8,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Website\Link as ProductWebsiteLink; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; /** * Product entity resource model @@ -101,6 +102,7 @@ class Product extends AbstractResource * @param \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes * @param array $data * @param TableMaintainer|null $tableMaintainer + * @param UniqueValidationInterface|null $uniqueValidator * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -115,7 +117,8 @@ public function __construct( \Magento\Eav\Model\Entity\TypeFactory $typeFactory, \Magento\Catalog\Model\Product\Attribute\DefaultAttributes $defaultAttributes, $data = [], - TableMaintainer $tableMaintainer = null + TableMaintainer $tableMaintainer = null, + UniqueValidationInterface $uniqueValidator = null ) { $this->_categoryCollectionFactory = $categoryCollectionFactory; $this->_catalogCategory = $catalogCategory; @@ -127,7 +130,8 @@ public function __construct( $context, $storeManager, $modelFactory, - $data + $data, + $uniqueValidator ); $this->connectionName = 'catalog'; $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); @@ -289,7 +293,7 @@ protected function _afterSave(\Magento\Framework\DataObject $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function delete($object) { @@ -575,7 +579,7 @@ public function countAll() } /** - * {@inheritdoc} + * @inheritdoc */ public function validate($object) { @@ -615,7 +619,7 @@ public function load($object, $entityId, $attributes = []) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @since 101.0.0 */ @@ -657,6 +661,8 @@ public function save(\Magento\Framework\Model\AbstractModel $object) } /** + * Retrieve entity manager object. + * * @return \Magento\Framework\EntityManager\EntityManager */ private function getEntityManager() @@ -669,6 +675,8 @@ private function getEntityManager() } /** + * Retrieve ProductWebsiteLink object. + * * @deprecated 101.1.0 * @return ProductWebsiteLink */ @@ -678,6 +686,8 @@ private function getProductWebsiteLink() } /** + * Retrieve CategoryLink object. + * * @deprecated 101.1.0 * @return \Magento\Catalog\Model\ResourceModel\Product\CategoryLink */ @@ -692,9 +702,10 @@ private function getProductCategoryLink() /** * Extends parent method to be appropriate for product. + * * Store id is required to correctly identify attribute value we are working with. * - * {@inheritdoc} + * @inheritdoc * @since 101.1.0 */ protected function getAttributeRow($entity, $object, $attribute) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php index 646cd0d4c1a4c..f7222bd41f42f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CustomOptionPriceModifier.php @@ -127,6 +127,8 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = } /** + * Check if custom options exist. + * * @param IndexTableStructure $priceTable * @return bool * @throws \Exception @@ -154,6 +156,8 @@ private function checkIfCustomOptionsExist(IndexTableStructure $priceTable): boo } /** + * Get connection. + * * @return \Magento\Framework\DB\Adapter\AdapterInterface */ private function getConnection() @@ -211,7 +215,7 @@ private function getSelectForOptionsWithMultipleValues(string $sourceTable): Sel } else { $select->joinLeft( ['otps' => $this->getTable('catalog_product_option_type_price')], - 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cwd.default_store_id', + 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cwd.default_store_id', [] ); @@ -373,6 +377,8 @@ private function getSelectAggregated(string $sourceTable): Select } /** + * Get select for update. + * * @param string $sourceTable * @return \Magento\Framework\DB\Select */ @@ -402,6 +408,8 @@ private function getSelectForUpdate(string $sourceTable): Select } /** + * Get table name. + * * @param string $tableName * @return string */ @@ -411,6 +419,8 @@ private function getTable(string $tableName): string } /** + * Is price scope global. + * * @return bool */ private function isPriceGlobal(): bool diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index 7ea85cd3f6f10..aab4577de3770 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -10,6 +10,7 @@ /** * Default Product Type Price Indexer Resource model + * * For correctly work need define product type id * * @api @@ -208,6 +209,8 @@ public function reindexEntity($entityIds) } /** + * Reindex prices. + * * @param null|int|array $entityIds * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice */ @@ -604,7 +607,7 @@ protected function _applyCustomOption() [] )->joinLeft( ['otps' => $this->getTable('catalog_product_option_type_price')], - 'otps.option_type_id = otpd.option_type_id AND otpd.store_id = cs.store_id', + 'otps.option_type_id = otpd.option_type_id AND otps.store_id = cs.store_id', [] )->group( ['i.entity_id', 'i.customer_group_id', 'i.website_id', 'o.option_id'] @@ -802,6 +805,8 @@ public function getIdxTable($table = null) } /** + * Check if product exists. + * * @return bool */ protected function hasEntity() @@ -823,6 +828,8 @@ protected function hasEntity() } /** + * Get total tier price expression. + * * @param \Zend_Db_Expr $priceExpression * @return \Zend_Db_Expr */ @@ -862,6 +869,13 @@ private function getTotalTierPriceExpression(\Zend_Db_Expr $priceExpression) ); } + /** + * Get tier price expression for table. + * + * @param string $tableAlias + * @param \Zend_Db_Expr $priceExpression + * @return \Zend_Db_Expr + */ private function getTierPriceExpressionForTable($tableAlias, \Zend_Db_Expr $priceExpression) { return $this->getConnection()->getCheckSql( diff --git a/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php new file mode 100644 index 0000000000000..ada96d4fd48f4 --- /dev/null +++ b/app/code/Magento/Catalog/Plugin/Model/Product/Option/UpdateProductCustomOptionsAttributes.php @@ -0,0 +1,59 @@ +productRepository = $productRepository; + } + + /** + * Update product 'has_options' and 'required_options' attributes after option save. + * + * @param \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject + * @param \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + * + * @return \Magento\Catalog\Api\Data\ProductCustomOptionInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface $subject, + \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option + ) { + $product = $this->productRepository->get($option->getProductSku()); + if (!$product->getHasOptions() + || ($option->getIsRequire() + && !$product->getRequiredOptions()) + ) { + $product->setCanSaveCustomOptions(true); + $product->setOptionsSaved(true); + $optionId = $option->getOptionId(); + $currentOptions = array_filter($product->getOptions(), function ($optionItem) use ($optionId) { + return $optionId != $optionItem->getOptionId(); + }); + $currentOptions[] = $option; + $product->setOptions($currentOptions); + $product->save(); + } + + return $option; + } +} diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php index 387ef9416ef68..a5e573caa381e 100644 --- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php +++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php @@ -29,8 +29,10 @@ public function __construct(CalculatorInterface $calculator) } /** - * Get raw value of "as low as" as a minimal among tier prices - * {@inheritdoc} + * Get raw value of "as low as" as a minimal among tier prices{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return float|null */ public function getValue(SaleableInterface $saleableItem) { @@ -49,8 +51,10 @@ public function getValue(SaleableInterface $saleableItem) } /** - * Return calculated amount object that keeps "as low as" value - * {@inheritdoc} + * Return calculated amount object that keeps "as low as" value{@inheritdoc} + * + * @param SaleableInterface $saleableItem + * @return AmountInterface|null */ public function getAmount(SaleableInterface $saleableItem) { @@ -58,6 +62,6 @@ public function getAmount(SaleableInterface $saleableItem) return $value === null ? null - : $this->calculator->getAmount($value, $saleableItem); + : $this->calculator->getAmount($value, $saleableItem, 'tax'); } } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml index 5c71342264212..11b5aabf0cee9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryProductAttributeActionGroup.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> @@ -15,15 +15,16 @@ - + - + - + - + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml index 1c6f115c2cfce..f043b82f2d44f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml @@ -74,4 +74,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index 2551bd61580ed..e46bfc7ee8b02 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -19,6 +19,12 @@ + + + + + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml index 7b01b6b4e5189..ec8a355523d63 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml @@ -16,6 +16,7 @@ 0 attributeThree + attributeThree 0 diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml index 0446a9db14f1a..8d588a192ed90 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductConfigurableAttributeData.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> Color 1 diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index c8c24d1f41e19..8ee8b49d0a10a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -7,7 +7,7 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> testSku simple @@ -295,6 +295,9 @@ 1 EavStock1 + + + 100 EavStock100 diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml new file mode 100644 index 0000000000000..000bb2095002c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinkData.xml @@ -0,0 +1,19 @@ + + + + + + + + related + simple + 1 + Qty1000 + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml new file mode 100644 index 0000000000000..bd4f807880ab8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductLinksData.xml @@ -0,0 +1,14 @@ + + + + + + RelatedProductLink + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml new file mode 100644 index 0000000000000..83f0a56c21545 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/WidgetsData.xml @@ -0,0 +1,15 @@ + + + + + + Catalog Product Link + Product Link Block Template + + diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml index 1bf7d0b0d988f..23be8408d4a1e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/product-meta.xml @@ -121,4 +121,7 @@ application/json + + application/json + diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml new file mode 100644 index 0000000000000..e23a503266e33 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminNewWidgetPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeNewPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeNewPage.xml new file mode 100644 index 0000000000000..9116e9ae7446f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductAttributeNewPage.xml @@ -0,0 +1,17 @@ + + + + +
+
+
+
+
+ + diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductUpdateAttributesPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductUpdateAttributesPage.xml new file mode 100644 index 0000000000000..84996a3814571 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductUpdateAttributesPage.xml @@ -0,0 +1,14 @@ + + + + +
+
+ + diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml index 9fcbcc199176b..fdfee62f6dc0b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/StorefrontProductPage.xml @@ -7,11 +7,12 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd">
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml new file mode 100644 index 0000000000000..5329ad48c8f43 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml new file mode 100644 index 0000000000000..0da67849f85c6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminNewWidgetSelectProductPopupSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml index 6844006e4e399..8e13f9c38f805 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFiltersSection.xml @@ -7,12 +7,13 @@ --> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml index 7a97a75556769..50b6fe6b3ec9e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml @@ -40,6 +40,7 @@ +
@@ -180,6 +181,7 @@ +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml index 1d49d05363612..90c3856933be9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml @@ -11,5 +11,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml new file mode 100644 index 0000000000000..051fda092d151 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection.xml @@ -0,0 +1,17 @@ + + + +
+ +
+
+ + +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index 1f1a4ce9133e7..3c769f9dbc0ce 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -22,5 +22,6 @@ +
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml new file mode 100644 index 0000000000000..a7b72bbaa78aa --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProducRelatedProductsSection.xml @@ -0,0 +1,14 @@ + + + + +
+ +
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductAvailableAfterEnablingSubCategoriesTest.xml new file mode 100644 index 0000000000000..fe7858313c848 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductAvailableAfterEnablingSubCategoriesTest.xml @@ -0,0 +1,54 @@ + + + + + + + + + <description value="Check that parent categories are showing products after enabling subcategories"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13914"/> + <useCaseId value="MAGETWO-96489"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SubCategoryWithParent" stepKey="createSubCategory"> + <requiredEntity createDataKey="createCategory"/> + <field key="is_active">false</field> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createSubCategory"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <!--Check anchor category is empty--> + <actionGroup ref="StorefrontCheckEmptyCategoryActionGroup" stepKey="checkEmptyAnchorCategory"> + <argument name="category" value="$$createCategory$$"/> + <argument name="productCount" value="0"/> + </actionGroup> + <!--Enable subcategory--> + <actionGroup ref="AdminNavigateToCategoryInTree" stepKey="openCreatedSubCategory"> + <argument name="category" value="$$createSubCategory$$"/> + </actionGroup> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="enableCategory"/> + <actionGroup ref="saveCategoryForm" stepKey="saveCategory"/> + <!--Check created product in anchor category on storefront--> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryStorefront"/> + <actionGroup ref="StorefrontCheckCategorySimpleProduct" stepKey="seeCreatedProduct"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml index 68dbe628e9817..e7054839ef4d0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest"> <annotations> <features value="Purchase a product with Custom Options on different Store Views"/> @@ -16,9 +16,6 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-77831"/> <group value="product"/> - <skip> - <issueId value="MAGETWO-98182"/> - </skip> </annotations> <before> @@ -65,18 +62,23 @@ <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView2"> <argument name="customStore" value="customStoreFR"/> </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearWebsitesGridFilters"/> + + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> + + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearProductsGridFilters"/> </after> <!-- Open Product Grid, Filter product and open --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> - <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <click selector="{{AdminProductGridSection.productGridXRowYColumnButton('1', '2')}}" stepKey="openProductForEdit"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad2"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> <!-- Update Product with Option Value DropDown 1--> @@ -100,15 +102,12 @@ <!-- Switcher to Store FR--> - <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> - - <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView(customStoreFR.name)}}" stepKey="clickStoreView"/> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptMessage"/> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToStoreFR"> + <argument name="scopeName" value="customStoreFR.name"/> + </actionGroup> <!-- Open tab Customizable Options --> - <waitForPageLoad time="10" stepKey="waitForPageLoad4"/> <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> <!-- Update Option Customizable Options and Option Value 1--> @@ -128,16 +127,13 @@ <!-- Login Customer Storefront --> - <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad6"/> - <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> - <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> - <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + <actionGroup ref="CustomerLoginOnStorefront" stepKey="customerLogin"> + <argument name="customer" value="$$createCustomer$$" /> + </actionGroup> <!-- Go to Product Page --> <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct1Page"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad7"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeproductOptionDropDownOptionTitle1"/> @@ -156,10 +152,7 @@ </actionGroup> <!-- Checking the correctness of displayed custom options for user parameters on checkout --> - - <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickCart"/> - <click selector="{{StorefrontMiniCartSection.goToCheckout}}" stepKey="goToCheckout"/> - <waitForPageLoad stepKey="s33"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> @@ -177,23 +170,29 @@ <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions1"/> <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="option2" stepKey="seeProductOptionValueDropdown1Input2"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> <!-- Place Order --> - <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder1"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> <!-- Open Order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> - <fillField selector="{{OrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> - <click selector="{{OrdersGridSection.submitSearch}}" stepKey="submitSearch"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> + <actionGroup ref="filterOrderGridById" stepKey="openOrdersGrid"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> <click selector="{{OrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> <waitForPageLoad time="30" stepKey="waitForPageLoad10"/> @@ -205,14 +204,12 @@ <!-- Switch to FR Store View Storefront --> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad11"/> - <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher1"/> - <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown1"/> - <click selector="{{StorefrontHeaderSection.storeViewOption(customStoreFR.code)}}" stepKey="selectStoreView1"/> - <waitForPageLoad stepKey="waitForPageLoad12"/> - <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad13"/> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStore"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDownTitle"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option1')}}" stepKey="productFrOptionDropDownOptionTitle1"/> @@ -232,9 +229,7 @@ <!-- Checking the correctness of displayed custom options for user parameters on checkout --> - <click selector="{{StorefrontMiniCartSection.show}}" stepKey="clickCart1"/> - <click selector="{{StorefrontMiniCartSection.goToCheckout}}" stepKey="goToCheckout1"/> - <waitForPageLoad stepKey="s34"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2" /> <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart1"/> @@ -252,18 +247,26 @@ <conditionalClick selector="{{CheckoutPaymentSection.productOptionsByProductItemPrice('150')}}" dependentSelector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" visible="false" stepKey="exposeProductOptions3"/> <see selector="{{CheckoutPaymentSection.productOptionsActiveByProductItemPrice('150')}}" userInput="FR option2" stepKey="seeProductFrOptionValueDropdown1Input3"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad14"/> <!-- Place Order --> - <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder2"/> - <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> + <!--Select shipping method--> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod2"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton2"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext2"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext2"/> + + <!--Select payment method--> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod2"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder2"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> <!-- Open Product Grid, Filter product and open --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad15"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions1"> <argument name="product" value="_defaultProduct"/> @@ -296,8 +299,7 @@ <!--Go to Product Page--> - <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page2"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad20"/> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProduct2Page2"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle1"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeProductOptionDropDownOptionTitle3"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php index 4602a0d99f6f1..2310b1f8b871c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/View/AttributesTest.php @@ -125,17 +125,28 @@ protected function setUp() } /** + * Get attribute with no value phrase + * + * @param string $phrase * @return void + * @dataProvider noValueProvider */ - public function testGetAttributeNoValue() + public function testGetAttributeNoValue(string $phrase) { - $this->phrase = ''; - $this->frontendAttribute - ->expects($this->any()) - ->method('getValue') - ->willReturn($this->phrase); + $this->frontendAttribute->method('getValue') + ->willReturn($phrase); $attributes = $this->attributesBlock->getAdditionalData(); - $this->assertTrue(empty($attributes['phrase'])); + $this->assertArrayNotHasKey('phrase', $attributes); + } + + /** + * No value data provider + * + * @return array + */ + public function noValueProvider(): array + { + return [[' '], ['']]; } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index aed87f918ebb8..c889c58e3df3a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -95,6 +95,14 @@ class HelperTest extends \PHPUnit\Framework\TestCase */ protected $attributeFilterMock; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dateTimeFilterMock; + + /** + * @inheritdoc + */ protected function setUp() { $this->objectManager = new ObjectManager($this); @@ -167,6 +175,11 @@ protected function setUp() $resolverProperty = $helperReflection->getProperty('linkResolver'); $resolverProperty->setAccessible(true); $resolverProperty->setValue($this->helper, $this->linkResolverMock); + + $this->dateTimeFilterMock = $this->createMock(\Magento\Framework\Stdlib\DateTime\Filter\DateTime::class); + $dateTimeFilterProperty = $helperReflection->getProperty('dateTimeFilter'); + $dateTimeFilterProperty->setAccessible(true); + $dateTimeFilterProperty->setValue($this->helper, $this->dateTimeFilterMock); } /** @@ -208,6 +221,12 @@ public function testInitialize( if (!empty($tierPrice)) { $productData = array_merge($productData, ['tier_price' => $tierPrice]); } + + $this->dateTimeFilterMock->expects($this->once()) + ->method('filter') + ->with($specialFromDate) + ->willReturn($specialFromDate); + $attributeNonDate = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php index a53b87dcf1567..c84753ad4adcb 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php @@ -348,13 +348,15 @@ public function testReindexFlatEnabled($flatScheduled, $productScheduled, $expec public function reindexFlatDisabledTestDataProvider() { return [ - [false, null, null, null, 0], - [true, null, null, null, 0], - [false, [], null, null, 0], - [false, ["1", "2"], null, null, 1], - [false, null, 1, null, 1], - [false, ["1", "2"], 0, 1, 1], - [false, null, 1, 1, 0], + [false, null, null, null, null, null, 0], + [true, null, null, null, null, null, 0], + [false, [], null, null, null, null, 0], + [false, ["1", "2"], null, null, null, null, 1], + [false, null, 1, null, null, null, 1], + [false, ["1", "2"], 0, 1, null, null, 1], + [false, null, 1, 1, null, null, 0], + [false, ["1", "2"], null, null, 0, 1, 1], + [false, ["1", "2"], null, null, 1, 0, 1], ]; } @@ -363,6 +365,8 @@ public function reindexFlatDisabledTestDataProvider() * @param array $affectedIds * @param int|string $isAnchorOrig * @param int|string $isAnchor + * @param mixed $isActiveOrig + * @param mixed $isActive, * @param int $expectedProductReindexCall * * @dataProvider reindexFlatDisabledTestDataProvider @@ -372,12 +376,16 @@ public function testReindexFlatDisabled( $affectedIds, $isAnchorOrig, $isAnchor, + $isActiveOrig, + $isActive, $expectedProductReindexCall ) { $this->category->setAffectedProductIds($affectedIds); $this->category->setData('is_anchor', $isAnchor); $this->category->setOrigData('is_anchor', $isAnchorOrig); $this->category->setAffectedProductIds($affectedIds); + $this->category->setData('is_active', $isActive); + $this->category->setOrigData('is_active', $isActiveOrig); $pathIds = ['path/1/2', 'path/2/3']; $this->category->setData('path_ids', $pathIds); @@ -387,7 +395,7 @@ public function testReindexFlatDisabled( ->method('isFlatEnabled') ->will($this->returnValue(false)); - $this->productIndexer->expects($this->exactly(1)) + $this->productIndexer->expects($this->any()) ->method('isScheduled') ->willReturn($productScheduled); $this->productIndexer->expects($this->exactly($expectedProductReindexCall)) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php index 034b04b6a757d..cfb54c3aefd0f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductOptions/Config/_files/invalidProductOptionsXmlArray.php @@ -29,12 +29,12 @@ ], ], 'renderer_attribute_with_invalid_value' => [ - '<?xml version="1.0"?><config><option name="name_one" renderer="true12"><inputType name="name_one"/>' . + '<?xml version="1.0"?><config><option name="name_one" renderer="123true"><inputType name="name_one"/>' . '</option></config>', [ - "Element 'option', attribute 'renderer': [facet 'pattern'] The value 'true12' is not accepted by the " . - "pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", - "Element 'option', attribute 'renderer': 'true12' is not a valid value of the atomic" . + "Element 'option', attribute 'renderer': [facet 'pattern'] The value '123true' is not accepted by the " . + "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'option', attribute 'renderer': '123true' is not a valid value of the atomic" . " type 'modelName'.\nLine: 1\n" ], ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php index e1847bea53fcb..868252da8190c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/invalidProductTypesXmlArray.php @@ -23,7 +23,7 @@ '<?xml version="1.0"?><config><type name="some_name" modelInstance="123" /></config>', [ "Element 'type', attribute 'modelInstance': [facet 'pattern'] The value '123' is not accepted by the" . - " pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + " pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'type', attribute 'modelInstance': '123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -57,7 +57,7 @@ '<?xml version="1.0"?><config><type name="some_name"><priceModel instance="123123" /></type></config>', [ "Element 'priceModel', attribute 'instance': [facet 'pattern'] The value '123123' is not accepted " . - "by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'priceModel', attribute 'instance': '123123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -66,7 +66,7 @@ '<?xml version="1.0"?><config><type name="some_name"><indexerModel instance="123" /></type></config>', [ "Element 'indexerModel', attribute 'instance': [facet 'pattern'] The value '123' is not accepted by " . - "the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'indexerModel', attribute 'instance': '123' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -83,7 +83,7 @@ '<?xml version="1.0"?><config><type name="some_name"><stockIndexerModel instance="1234"/></type></config>', [ "Element 'stockIndexerModel', attribute 'instance': [facet 'pattern'] The value '1234' is not " . - "accepted by the pattern '[a-zA-Z_\\\\]+'.\nLine: 1\n", + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'stockIndexerModel', attribute 'instance': '1234' is not a valid value of the atomic " . "type 'modelName'.\nLine: 1\n" ], diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml index 7edbc399a9476..701338774baa5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTypes/Config/_files/valid_product_types_merged.xml @@ -15,6 +15,14 @@ <stockIndexerModel instance="instance_name"/> </type> <type label="some_label" name="some_name2" modelInstance="model_name"> + <allowedSelectionTypes> + <type name="some_name" /> + </allowedSelectionTypes> + <priceModel instance="instance_name_with_digits_123" /> + <indexerModel instance="instance_name_with_digits_123" /> + <stockIndexerModel instance="instance_name_with_digits_123"/> + </type> + <type label="some_label" name="some_name3" modelInstance="model_name"> <allowedSelectionTypes> <type name="some_name" /> </allowedSelectionTypes> @@ -25,5 +33,6 @@ <composableTypes> <type name="some_name"/> <type name="some_name2"/> + <type name="some_name3"/> </composableTypes> </config> diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php new file mode 100644 index 0000000000000..c4a7aa4037bec --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/Component/ColumnFactoryTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\Component; + +use PHPUnit\Framework\TestCase; +use Magento\Catalog\Ui\Component\ColumnFactory; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\ColumnInterface; +use Magento\Ui\Component\Filters\FilterModifier; + +/** + * ColumnFactory test. + */ +class ColumnFactoryTest extends TestCase +{ + /** + * @var ColumnFactory + */ + private $columnFactory; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductAttributeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $attribute; + + /** + * @var ContextInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $context; + + /** + * @var UiComponentFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $uiComponentFactory; + + /** + * @var ColumnInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $column; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = new ObjectManager($this); + + $this->attribute = $this->getMockBuilder(ProductAttributeInterface::class) + ->setMethods(['usesSource']) + ->getMockForAbstractClass(); + $this->context = $this->createMock(ContextInterface::class); + $this->uiComponentFactory = $this->createMock(UiComponentFactory::class); + $this->column = $this->getMockForAbstractClass(ColumnInterface::class); + $this->uiComponentFactory->method('create') + ->willReturn($this->column); + + $this->columnFactory = $this->objectManager->getObject(ColumnFactory::class, [ + 'componentFactory' => $this->uiComponentFactory + ]); + } + + /** + * Tests the create method will return correct object. + * + * @return void + */ + public function testCreatedObject() + { + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn([]); + + $object = $this->columnFactory->create($this->attribute, $this->context); + $this->assertEquals( + $this->column, + $object, + 'Object must be the same which the ui component factory creates.' + ); + } + + /** + * Tests create method with not filterable in grid attribute. + * + * @param array $filterModifiers + * @param null|string $filter + * + * @return void + * @dataProvider filterModifiersProvider + */ + public function testCreateWithNotFilterableInGridAttribute(array $filterModifiers, $filter) + { + $componentFactoryArgument = [ + 'data' => [ + 'config' => [ + 'label' => __(null), + 'dataType' => 'text', + 'add_field' => true, + 'visible' => null, + 'filter' => $filter, + 'component' => 'Magento_Ui/js/grid/columns/column', + ], + ], + 'context' => $this->context, + ]; + + $this->context->method('getRequestParam') + ->with(FilterModifier::FILTER_MODIFIER, []) + ->willReturn($filterModifiers); + $this->attribute->method('getIsFilterableInGrid') + ->willReturn(false); + $this->attribute->method('getAttributeCode') + ->willReturn('color'); + + $this->uiComponentFactory->expects($this->once()) + ->method('create') + ->with($this->anything(), $this->anything(), $componentFactoryArgument); + + $this->columnFactory->create($this->attribute, $this->context); + } + + /** + * Filter modifiers data provider. + * + * @return array + */ + public function filterModifiersProvider(): array + { + return [ + 'without' => [ + 'filter_modifiers' => [], + 'filter' => null, + ], + 'with' => [ + 'filter_modifiers' => [ + 'color' => [ + 'condition_type' => 'notnull', + ], + ], + 'filter' => 'text', + ], + ]; + } +} diff --git a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php index cbc67fee8a5a3..40687e37e1538 100644 --- a/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php +++ b/app/code/Magento/Catalog/Ui/Component/ColumnFactory.php @@ -5,6 +5,8 @@ */ namespace Magento\Catalog\Ui\Component; +use Magento\Ui\Component\Filters\FilterModifier; + /** * @api * @since 100.0.2 @@ -54,13 +56,15 @@ public function __construct(\Magento\Framework\View\Element\UiComponentFactory $ */ public function create($attribute, $context, array $config = []) { + $filterModifiers = $context->getRequestParam(FilterModifier::FILTER_MODIFIER, []); + $columnName = $attribute->getAttributeCode(); $config = array_merge([ 'label' => __($attribute->getDefaultFrontendLabel()), 'dataType' => $this->getDataType($attribute), 'add_field' => true, 'visible' => $attribute->getIsVisibleInGrid(), - 'filter' => ($attribute->getIsFilterableInGrid()) + 'filter' => ($attribute->getIsFilterableInGrid() || array_key_exists($columnName, $filterModifiers)) ? $this->getFilterType($attribute->getFrontendInput()) : null, ], $config); diff --git a/app/code/Magento/Catalog/etc/product_options.xsd b/app/code/Magento/Catalog/etc/product_options.xsd index 3bc24a9099262..734c8f378d5d7 100644 --- a/app/code/Magento/Catalog/etc/product_options.xsd +++ b/app/code/Magento/Catalog/etc/product_options.xsd @@ -61,11 +61,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/product_types_base.xsd b/app/code/Magento/Catalog/etc/product_types_base.xsd index 6cc35fd7bee37..dec952bcf492e 100644 --- a/app/code/Magento/Catalog/etc/product_types_base.xsd +++ b/app/code/Magento/Catalog/etc/product_types_base.xsd @@ -92,11 +92,11 @@ <xs:simpleType name="modelName"> <xs:annotation> <xs:documentation> - Model name can contain only [a-zA-Z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. </xs:documentation> </xs:annotation> <xs:restriction base="xs:string"> - <xs:pattern value="[a-zA-Z_\\]+" /> + <xs:pattern value="([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+" /> </xs:restriction> </xs:simpleType> </xs:schema> diff --git a/app/code/Magento/Catalog/etc/webapi_rest/di.xml b/app/code/Magento/Catalog/etc/webapi_rest/di.xml index 49c5eff91ee49..7c86c1f2e3929 100644 --- a/app/code/Magento/Catalog/etc/webapi_rest/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_rest/di.xml @@ -20,4 +20,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi_soap/di.xml b/app/code/Magento/Catalog/etc/webapi_soap/di.xml index 2a5d60222e9f8..44cdd473bf74e 100644 --- a/app/code/Magento/Catalog/etc/webapi_soap/di.xml +++ b/app/code/Magento/Catalog/etc/webapi_soap/di.xml @@ -19,4 +19,7 @@ <plugin name="get_catalog_category_product_index_table_name" type="Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver"/> <plugin name="get_catalog_product_price_index_table_name" type="Magento\Catalog\Model\Indexer\Product\Price\Plugin\TableResolver"/> </type> + <type name="Magento\Catalog\Api\ProductCustomOptionRepositoryInterface"> + <plugin name="updateProductCustomOptionsAttributes" type="Magento\Catalog\Plugin\Model\Product\Option\UpdateProductCustomOptionsAttributes"/> + </type> </config> 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 efc06d675c369..64c8ba7dcf49f 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 @@ -30,6 +30,13 @@ }); </script> +<?php +$defaultMinSaleQty = $block->getDefaultConfigValue('min_sale_qty'); +if (!is_numeric($defaultMinSaleQty)) { + $defaultMinSaleQty = json_decode($defaultMinSaleQty, true); + $defaultMinSaleQty = (float) $defaultMinSaleQty[\Magento\Customer\Api\Data\GroupInterface::CUST_GROUP_ALL] ?? 1; +} +?> <div class="fieldset-wrapper form-inline advanced-inventory-edit"> <div class="fieldset-wrapper-title"> <strong class="title"> @@ -132,7 +139,7 @@ <div class="field"> <input type="text" class="input-text validate-number" id="inventory_min_sale_qty" name="<?= /* @escapeNotVerified */ $block->getFieldSuffix() ?>[min_sale_qty]" - value="<?= /* @escapeNotVerified */ $block->getDefaultConfigValue('min_sale_qty') * 1 ?>" + value="<?= /* @escapeNotVerified */ $defaultMinSaleQty ?>" disabled="disabled"/> </div> <div class="field choice"> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml index baed8c1ce04de..71452a2d65e97 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/addtocart.phtml @@ -33,7 +33,7 @@ <button type="submit" title="<?= /* @escapeNotVerified */ $buttonTitle ?>" class="action primary tocart" - id="product-addtocart-button"> + id="product-addtocart-button" disabled> <span><?= /* @escapeNotVerified */ $buttonTitle ?></span> </button> <?= $block->getChildHtml('', true) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml index 038bea86e7d4e..22860418b157d 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/details.phtml @@ -21,17 +21,17 @@ $label = $block->getChildData($alias, 'title'); ?> <div class="data item title" - aria-labelledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" data-role="collapsible" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>"> <a class="data switch" tabindex="-1" - data-toggle="switch" + data-toggle="trigger" href="#<?= /* @escapeNotVerified */ $alias ?>" id="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title"> <?= /* @escapeNotVerified */ $label ?> </a> </div> - <div class="data item content" id="<?= /* @escapeNotVerified */ $alias ?>" data-role="content"> + <div class="data item content" + aria-labelledby="tab-label-<?= /* @escapeNotVerified */ $alias ?>-title" id="<?= /* @escapeNotVerified */ $alias ?>" data-role="content"> <?= /* @escapeNotVerified */ $html ?> </div> <?php endforeach;?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml index a2b91a5eeb99f..40f86c7e68d6c 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml @@ -14,7 +14,7 @@ <meta property="og:image" content="<?= $block->escapeUrl($block->getImage($block->getProduct(), 'product_base_image')->getImageUrl()) ?>" /> <meta property="og:description" content="<?= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getShortDescription())) ?>" /> <meta property="og:url" content="<?= $block->escapeUrl($block->getProduct()->getProductUrl()) ?>" /> -<?php if ($priceAmount = $block->getProduct()->getFinalPrice()):?> +<?php if ($priceAmount = $block->getProduct()->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)->getAmount()):?> <meta property="product:price:amount" content="<?= /* @escapeNotVerified */ $priceAmount ?>"/> <?= $block->getChildHtml('meta.currency') ?> <?php endif;?> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js index c0637cb672dc6..755e777a01f77 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/validate-product.js @@ -13,7 +13,8 @@ define([ $.widget('mage.productValidate', { options: { bindSubmit: false, - radioCheckboxClosest: '.nested' + radioCheckboxClosest: '.nested', + addToCartButtonSelector: '.action.tocart' }, /** @@ -41,6 +42,7 @@ define([ return false; } }); + $(this.options.addToCartButtonSelector).attr('disabled', false); } }); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 3578123e94dc1..9cb6d4bd2390d 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -3,12 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogImportExport\Model\Import; use Magento\Catalog\Model\Config as CatalogConfig; use Magento\Catalog\Model\Product\Visibility; use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; +use Magento\CatalogImportExport\Model\StockItemImporterInterface; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; @@ -638,6 +640,13 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $_logger; + /** + * "Duplicate multiselect values" error array key + * + * @var string + */ + private static $errorDuplicateMultiselectValues = 'duplicatedMultiselectValues'; + /** * {@inheritdoc} */ @@ -767,7 +776,6 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param CatalogConfig $catalogConfig * @param MediaGalleryProcessor $mediaProcessor * @param ProductRepositoryInterface|null $productRepository - * @throws \Magento\Framework\Exception\LocalizedException * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -857,11 +865,8 @@ public function __construct( $string, $errorAggregator ); - $this->_optionEntity = isset( - $data['option_entity'] - ) ? $data['option_entity'] : $optionFactory->create( - ['data' => ['product_entity' => $this]] - ); + $this->_optionEntity = $data['option_entity'] + ?? $optionFactory->create(['data' => ['product_entity' => $this]]); $this->_initAttributeSets() ->_initTypeModels() @@ -869,6 +874,9 @@ public function __construct( $this->validator->init($this); $this->productRepository = $productRepository ?? \Magento\Framework\App\ObjectManager::getInstance() ->get(ProductRepositoryInterface::class); + + $errorMessageText = __('Value for multiselect attribute %s contains duplicated values'); + $this->_messageTemplates[self::$errorDuplicateMultiselectValues] = $errorMessageText; } /** @@ -884,7 +892,7 @@ public function isAttributeValid($attrCode, array $attrParams, array $rowData, $ { if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) { foreach ($this->validator->getMessages() as $message) { - $this->addRowError($message, $rowNum, $attrCode); + $this->skipRow($rowNum, $message, ProcessingError::ERROR_LEVEL_NOT_CRITICAL, $attrCode); } return false; } @@ -1592,7 +1600,7 @@ protected function _saveProducts() if (!empty($rowData[self::URL_KEY])) { // If url_key column and its value were in the CSV file $rowData[self::URL_KEY] = $urlKey; - } else if ($this->isNeedToChangeUrlKey($rowData)) { + } elseif ($this->isNeedToChangeUrlKey($rowData)) { // If url_key column was empty or even not declared in the CSV file but by the rules it is need to // be setteed. In case when url_key is generating from name column we have to ensure that the bunch // of products will pass for the event with url_key column. @@ -1604,7 +1612,9 @@ protected function _saveProducts() if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); continue; - } elseif (self::SCOPE_STORE == $rowScope) { + } + + if (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id']; $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; @@ -1740,13 +1750,7 @@ protected function _saveProducts() $uploadedImages[$columnImage] = $uploadedFile; } else { unset($rowData[$column]); - $this->addRowError( - ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, - $rowNum, - null, - null, - ProcessingError::ERROR_LEVEL_NOT_CRITICAL - ); + $this->skipRow($rowNum, ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE); } } else { $uploadedFile = $uploadedImages[$columnImage]; @@ -2380,32 +2384,35 @@ public function validateRow(array $rowData, $rowNum) // BEHAVIOR_DELETE and BEHAVIOR_REPLACE use specific validation logic if (Import::BEHAVIOR_REPLACE == $this->getBehavior()) { if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); return false; } } if (Import::BEHAVIOR_DELETE == $this->getBehavior()) { if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { - $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); return false; } return true; } + // if product doesn't exist, need to throw critical error else all errors should be not critical. + $errorLevel = $this->getValidationErrorLevel($sku); + if (!$this->validator->isValid($rowData)) { foreach ($this->validator->getMessages() as $message) { - $this->addRowError($message, $rowNum, $this->validator->getInvalidAttribute()); + $this->skipRow($rowNum, $message, $errorLevel, $this->validator->getInvalidAttribute()); } } if (null === $sku) { - $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_IS_EMPTY, $errorLevel); } elseif (false === $sku) { - $this->addRowError(ValidatorInterface::ERROR_ROW_IS_ORPHAN, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_ROW_IS_ORPHAN, $errorLevel); } elseif (self::SCOPE_STORE == $rowScope && !$this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) ) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_STORE, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_STORE, $errorLevel); } // SKU is specified, row is SCOPE_DEFAULT, new product block begins @@ -2420,19 +2427,15 @@ public function validateRow(array $rowData, $rowNum) $this->prepareNewSkuData($sku) ); } else { - $this->addRowError(ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $errorLevel); } } else { // validate new product type and attribute set - if (!isset($rowData[self::COL_TYPE]) || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_TYPE, $rowNum); - } elseif (!isset( - $rowData[self::COL_ATTR_SET] - ) || !isset( - $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]] - ) + if (!isset($rowData[self::COL_TYPE], $this->_productTypeModels[$rowData[self::COL_TYPE]])) { + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_TYPE, $errorLevel); + } elseif (!isset($rowData[self::COL_ATTR_SET], $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) ) { - $this->addRowError(ValidatorInterface::ERROR_INVALID_ATTR_SET, $rowNum); + $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_ATTR_SET, $errorLevel); } elseif (is_null($this->skuProcessor->getNewSku($sku))) { $this->skuProcessor->addNewSku( $sku, @@ -2488,8 +2491,11 @@ public function validateRow(array $rowData, $rowNum) ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, $rowData[self::COL_NAME], - $message - ); + $message, + $errorLevel + ) + ->getErrorAggregator() + ->addRowToSkip($rowNum); } } } @@ -2499,9 +2505,10 @@ public function validateRow(array $rowData, $rowNum) $newFromTimestamp = strtotime($this->dateTime->formatDate($rowData[self::COL_NEW_FROM_DATE], false)); $newToTimestamp = strtotime($this->dateTime->formatDate($rowData[self::COL_NEW_TO_DATE], false)); if ($newFromTimestamp > $newToTimestamp) { - $this->addRowError( - ValidatorInterface::ERROR_NEW_TO_DATE, + $this->skipRow( $rowNum, + ValidatorInterface::ERROR_NEW_TO_DATE, + $errorLevel, $rowData[self::COL_NEW_TO_DATE] ); } @@ -3029,4 +3036,39 @@ private function retrieveProductBySku(string $sku) return $product; } + + /** + * Add row as skipped + * + * @param int $rowNum + * @param string $errorCode Error code or simply column name + * @param string $errorLevel error level + * @param string|null $colName optional column name + * @return $this + */ + private function skipRow( + int $rowNum, + string $errorCode, + string $errorLevel = ProcessingError::ERROR_LEVEL_NOT_CRITICAL, + $colName = null + ): self { + $this->addRowError($errorCode, $rowNum, $colName, null, $errorLevel); + $this->getErrorAggregator() + ->addRowToSkip($rowNum); + + return $this; + } + + /** + * Returns errorLevel for validation + * + * @param string|bool|null $sku + * @return string + */ + private function getValidationErrorLevel($sku): string + { + return (!$this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) + ? ProcessingError::ERROR_LEVEL_CRITICAL + : ProcessingError::ERROR_LEVEL_NOT_CRITICAL; + } } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index 1cd19852f393c..3f72fcc39f548 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogImportExport\Test\Unit\Model\Import; +use Magento\CatalogImportExport\Model\Import\Product; +use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Import; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class ProductTest @@ -25,126 +29,126 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI const ENTITY_ID = 13; - /** @var \Magento\Framework\DB\Adapter\AdapterInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\DB\Adapter\AdapterInterface| MockObject */ protected $_connection; - /** @var \Magento\Framework\Json\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Json\Helper\Data| MockObject */ protected $jsonHelper; - /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data| MockObject */ protected $_dataSourceModel; - /** @var \Magento\Framework\App\ResourceConnection|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\App\ResourceConnection| MockObject */ protected $resource; - /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Helper| MockObject */ protected $_resourceHelper; - /** @var \Magento\Framework\Stdlib\StringUtils|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\StringUtils|MockObject */ protected $string; - /** @var \Magento\Framework\Event\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Event\ManagerInterface|MockObject */ protected $_eventManager; - /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\StockRegistryInterface|MockObject */ protected $stockRegistry; - /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\OptionFactory|MockObject */ protected $optionFactory; - /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Api\StockConfigurationInterface|MockObject */ protected $stockConfiguration; - /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface|MockObject */ protected $stockStateProvider; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ protected $optionEntity; - /** @var \Magento\Framework\Stdlib\DateTime|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\DateTime|MockObject */ protected $dateTime; /** @var array */ protected $data; - /** @var \Magento\ImportExport\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Helper\Data|MockObject */ protected $importExportData; - /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Import\Data|MockObject */ protected $importData; - /** @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Eav\Model\Config|MockObject */ protected $config; - /** @var \Magento\ImportExport\Model\ResourceModel\Helper|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\ResourceModel\Helper|MockObject */ protected $resourceHelper; - /** @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Catalog\Helper\Data|MockObject */ protected $_catalogData; - /** @var \Magento\ImportExport\Model\Import\Config|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\ImportExport\Model\Import\Config|MockObject */ protected $_importConfig; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** @var MockObject */ protected $_resourceFactory; // @codingStandardsIgnoreStart - /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory|MockObject */ protected $_setColFactory; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory|MockObject */ protected $_productTypeFactory; - /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory|MockObject */ protected $_linkFactory; - /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory|MockObject */ protected $_proxyProdFactory; - /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\UploaderFactory|MockObject */ protected $_uploaderFactory; - /** @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Filesystem|MockObject */ protected $_filesystem; - /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Filesystem\Directory\WriteInterface|MockObject */ protected $_mediaDirectory; - /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory|MockObject */ protected $_stockResItemFac; - /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface|MockObject */ protected $_localeDate; - /** @var \Magento\Framework\Indexer\IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Indexer\IndexerRegistry|MockObject */ protected $indexerRegistry; - /** @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Psr\Log\LoggerInterface|MockObject */ protected $_logger; - /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver|MockObject */ protected $storeResolver; - /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\SkuProcessor|MockObject */ protected $skuProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor|MockObject */ protected $categoryProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ protected $validator; - /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor|MockObject */ protected $objectRelationProcessor; - /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface|MockObject */ protected $transactionManager; - /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|\PHPUnit_Framework_MockObject_MockObject */ + /** @var \Magento\CatalogImportExport\Model\Import\Product\TaxClassProcessor|MockObject */ // @codingStandardsIgnoreEnd protected $taxClassProcessor; - /** @var \Magento\CatalogImportExport\Model\Import\Product */ + /** @var Product */ protected $importProduct; /** @@ -152,10 +156,10 @@ class ProductTest extends \Magento\ImportExport\Test\Unit\Model\Import\AbstractI */ protected $errorAggregator; - /** @var \Magento\Framework\App\Config\ScopeConfigInterface|\PHPUnit_Framework_MockObject_MockObject*/ + /** @var \Magento\Framework\App\Config\ScopeConfigInterface|MockObject */ protected $scopeConfig; - /** @var \Magento\Catalog\Model\Product\Url|\PHPUnit_Framework_MockObject_MockObject*/ + /** @var \Magento\Catalog\Model\Product\Url|MockObject */ protected $productUrl; /** @@ -334,7 +338,7 @@ protected function setUp() $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); $this->importProduct = $objectManager->getObject( - \Magento\CatalogImportExport\Model\Import\Product::class, + Product::class, [ 'jsonHelper' => $this->jsonHelper, 'importExportData' => $this->importExportData, @@ -375,7 +379,7 @@ protected function setUp() 'data' => $this->data ] ); - $reflection = new \ReflectionClass(\Magento\CatalogImportExport\Model\Import\Product::class); + $reflection = new \ReflectionClass(Product::class); $reflectionProperty = $reflection->getProperty('metadataPool'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($this->importProduct, $metadataPoolMock); @@ -584,7 +588,7 @@ public function testGetMultipleValueSeparatorFromParameters() public function testDeleteProductsForReplacement() { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods([ 'setParameters', '_deleteProducts' @@ -650,7 +654,7 @@ public function testValidateRowIsAlreadyValidated() */ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour = Import::BEHAVIOR_DELETE) { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods(['getBehavior', 'getRowScope', 'getErrorAggregator']) ->getMock(); @@ -662,7 +666,7 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour ->method('getErrorAggregator') ->willReturn($this->getErrorAggregatorObject()); $importProduct->expects($this->once())->method('getRowScope')->willReturn($rowScope); - $skuKey = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; + $skuKey = Product::COL_SKU; $rowData = [ $skuKey => 'sku', ]; @@ -674,18 +678,22 @@ public function testValidateRow($rowScope, $oldSku, $expectedResult, $behaviour public function testValidateRowDeleteBehaviourAddRowErrorCall() { - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getBehavior', 'getRowScope', 'addRowError']) + ->setMethods(['getBehavior', 'getRowScope', 'addRowError', 'getErrorAggregator']) ->getMock(); $importProduct->expects($this->exactly(2))->method('getBehavior') ->willReturn(\Magento\ImportExport\Model\Import::BEHAVIOR_DELETE); $importProduct->expects($this->once())->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT); + ->willReturn(Product::SCOPE_DEFAULT); $importProduct->expects($this->once())->method('addRowError'); + $importProduct->method('getErrorAggregator') + ->willReturn( + $this->getErrorAggregatorObject(['addRowToSkip']) + ); $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', + Product::COL_SKU => 'sku', ]; $importProduct->validateRow($rowData, 0); @@ -696,7 +704,7 @@ public function testValidateRowValidatorCheck() $messages = ['validator message']; $this->validator->expects($this->once())->method('getMessages')->willReturn($messages); $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => 'sku', + Product::COL_SKU => 'sku', ]; $rowNum = 0; $this->importProduct->validateRow($rowData, $rowNum); @@ -798,7 +806,7 @@ public function getStoreIdByCodeDataProvider() return [ [ '$storeCode' => null, - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$expectedResult' => Product::SCOPE_DEFAULT, ], [ '$storeCode' => 'value', @@ -813,17 +821,17 @@ public function getStoreIdByCodeDataProvider() public function testValidateRowCheckSpecifiedSku($sku, $expectedError) { $importProduct = $this->createModelMockWithErrorAggregator( - [ 'addRowError', 'getOptionEntity', 'getRowScope'], + ['addRowError', 'getOptionEntity', 'getRowScope'], ['isRowInvalid' => true] ); $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_STORE => '', + Product::COL_SKU => $sku, + Product::COL_STORE => '', ]; - $this->storeResolver->expects($this->any())->method('getStoreCodeToId')->willReturn(null); + $this->storeResolver->method('getStoreCodeToId')->willReturn(null); $this->setPropertyValue($importProduct, 'storeResolver', $this->storeResolver); $this->setPropertyValue($importProduct, 'skuProcessor', $this->skuProcessor); @@ -832,7 +840,7 @@ public function testValidateRowCheckSpecifiedSku($sku, $expectedError) $importProduct ->expects($this->once()) ->method('getRowScope') - ->willReturn(\Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE); + ->willReturn(Product::SCOPE_STORE); $importProduct->expects($this->at(1))->method('addRowError')->with($expectedError, $rowNum)->willReturn(null); $importProduct->validateRow($rowData, $rowNum); @@ -846,7 +854,7 @@ public function testValidateRowProcessEntityIncrement() $errorAggregator->method('isRowInvalid')->willReturn(true); $this->setPropertyValue($this->importProduct, '_processedEntitiesCount', $count); $this->setPropertyValue($this->importProduct, 'errorAggregator', $errorAggregator); - $rowData = [\Magento\CatalogImportExport\Model\Import\Product::COL_SKU => false]; + $rowData = [Product::COL_SKU => false]; //suppress validator $this->_setValidatorMockInImportProduct($this->importProduct); $this->importProduct->validateRow($rowData, $rowNum); @@ -856,14 +864,14 @@ public function testValidateRowProcessEntityIncrement() public function testValidateRowValidateExistingProductTypeAddNewSku() { $importProduct = $this->createModelMockWithErrorAggregator( - [ 'addRowError', 'getOptionEntity'], + ['addRowError', 'getOptionEntity'], ['isRowInvalid' => true] ); $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + Product::COL_SKU => $sku, ]; $oldSku = [ $sku => [ @@ -886,7 +894,7 @@ public function testValidateRowValidateExistingProductTypeAddNewSku() $this->setPropertyValue($importProduct, '_oldSku', $oldSku); $expectedData = [ - 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val + 'entity_id' => $oldSku[$sku]['entity_id'], //entity_id_val 'type_id' => $oldSku[$sku]['type_id'],// type_id_val 'attr_set_id' => $oldSku[$sku]['attr_set_id'], //attr_set_id_val 'attr_set_code' => $_attrSetIdToName[$oldSku[$sku]['attr_set_id']],//attr_set_id_val @@ -904,7 +912,7 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, + Product::COL_SKU => $sku, ]; $oldSku = [ $sku => [ @@ -929,6 +937,11 @@ public function testValidateRowValidateExistingProductTypeAddErrorRowCall() /** * @dataProvider validateRowValidateNewProductTypeAddRowErrorCallDataProvider + * @param string $colType + * @param string $productTypeModelsColType + * @param string $colAttrSet + * @param string $attrSetNameToIdColAttrSet + * @param string $error */ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $colType, @@ -940,15 +953,15 @@ public function testValidateRowValidateNewProductTypeAddRowErrorCall( $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => $colType, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $colAttrSet, + Product::COL_SKU => $sku, + Product::COL_TYPE => $colType, + Product::COL_ATTR_SET => $colAttrSet, ]; $_attrSetNameToId = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, + $rowData[Product::COL_ATTR_SET] => $attrSetNameToIdColAttrSet, ]; $_productTypeModels = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => $productTypeModelsColType, + $rowData[Product::COL_TYPE] => $productTypeModelsColType, ]; $oldSku = [ $sku => null, @@ -976,29 +989,25 @@ public function testValidateRowValidateNewProductTypeGetNewSkuCall() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_TYPE => 'value', - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'value', + Product::COL_SKU => $sku, + Product::COL_TYPE => 'value', + Product::COL_ATTR_SET => 'value', ]; $_productTypeModels = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE] => 'value', + $rowData[Product::COL_TYPE] => 'value', ]; $oldSku = [ $sku => null, ]; $_attrSetNameToId = [ - $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => 'attr_set_code_val' + $rowData[Product::COL_ATTR_SET] => 'attr_set_code_val', ]; $expectedData = [ 'entity_id' => null, - 'type_id' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_TYPE],//value + 'type_id' => $rowData[Product::COL_TYPE], //attr_set_id_val - 'attr_set_id' => $_attrSetNameToId[ - $rowData[ - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET - ] - ], - 'attr_set_code' => $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET],//value + 'attr_set_id' => $_attrSetNameToId[$rowData[Product::COL_ATTR_SET]], + 'attr_set_code' => $rowData[Product::COL_ATTR_SET], 'row_id' => null ]; $importProduct = $this->createModelMockWithErrorAggregator( @@ -1034,8 +1043,8 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => 'col_attr_set_val', ]; $expectedAttrSetCode = 'new_attr_set_code'; $newSku = [ @@ -1043,8 +1052,8 @@ public function testValidateRowSetAttributeSetCodeIntoRowData() 'type_id' => 'new_type_id_val', ]; $expectedRowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => $newSku['attr_set_code'], + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => $newSku['attr_set_code'], ]; $oldSku = [ $sku => [ @@ -1078,8 +1087,8 @@ public function testValidateValidateOptionEntity() $sku = 'sku'; $rowNum = 0; $rowData = [ - \Magento\CatalogImportExport\Model\Import\Product::COL_SKU => $sku, - \Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET => 'col_attr_set_val', + Product::COL_SKU => $sku, + Product::COL_ATTR_SET => 'col_attr_set_val', ]; $oldSku = [ $sku => [ @@ -1336,7 +1345,7 @@ public function validateRowDataProvider() { return [ [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => null, '$expectedResult' => false, ], @@ -1351,12 +1360,12 @@ public function validateRowDataProvider() '$expectedResult' => true, ], [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => true, '$expectedResult' => true, ], [ - '$rowScope' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT, + '$rowScope' => Product::SCOPE_DEFAULT, '$oldSku' => null, '$expectedResult' => false, '$behaviour' => Import::BEHAVIOR_REPLACE @@ -1377,7 +1386,7 @@ public function isAttributeValidAssertAttrValidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH - 1 + Product::DB_MAX_VARCHAR_LENGTH - 1 ), ], ], @@ -1430,7 +1439,7 @@ public function isAttributeValidAssertAttrValidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH - 1 + Product::DB_MAX_TEXT_LENGTH - 1 ), ], ], @@ -1450,7 +1459,7 @@ public function isAttributeValidAssertAttrInvalidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_VARCHAR_LENGTH + 1 + Product::DB_MAX_VARCHAR_LENGTH + 1 ), ], ], @@ -1503,7 +1512,7 @@ public function isAttributeValidAssertAttrInvalidDataProvider() '$rowData' => [ 'code' => str_repeat( 'a', - \Magento\CatalogImportExport\Model\Import\Product::DB_MAX_TEXT_LENGTH + 1 + Product::DB_MAX_TEXT_LENGTH + 1 ), ], ], @@ -1515,8 +1524,8 @@ public function isAttributeValidAssertAttrInvalidDataProvider() */ public function getRowScopeDataProvider() { - $colSku = \Magento\CatalogImportExport\Model\Import\Product::COL_SKU; - $colStore = \Magento\CatalogImportExport\Model\Import\Product::COL_STORE; + $colSku = Product::COL_SKU; + $colStore = Product::COL_STORE; return [ [ @@ -1524,21 +1533,21 @@ public function getRowScopeDataProvider() $colSku => null, $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE + '$expectedResult' => Product::SCOPE_STORE, ], [ '$rowData' => [ $colSku => 'sku', $colStore => null, ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_DEFAULT + '$expectedResult' => Product::SCOPE_DEFAULT, ], [ '$rowData' => [ $colSku => 'sku', $colStore => 'store', ], - '$expectedResult' => \Magento\CatalogImportExport\Model\Import\Product::SCOPE_STORE + '$expectedResult' => Product::SCOPE_STORE, ], ]; } @@ -1615,9 +1624,9 @@ protected function overrideMethod(&$object, $methodName, array $parameters = []) * * @see _rewriteGetOptionEntityInImportProduct() * @see _setValidatorMockInImportProduct() - * @param \Magento\CatalogImportExport\Model\Import\Product + * @param Product * Param should go with rewritten getOptionEntity method. - * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) { @@ -1633,8 +1642,8 @@ private function _suppressValidateRowOptionValidatorInvalidRows($importProduct) * Used in group of validateRow method's tests. * Set validator mock in importProduct, return true for isValid method. * - * @param \Magento\CatalogImportExport\Model\Import\Product - * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|\PHPUnit_Framework_MockObject_MockObject + * @param Product + * @return \Magento\CatalogImportExport\Model\Import\Product\Validator|MockObject */ private function _setValidatorMockInImportProduct($importProduct) { @@ -1648,9 +1657,9 @@ private function _setValidatorMockInImportProduct($importProduct) * Used in group of validateRow method's tests. * Make getOptionEntity return option mock. * - * @param \Magento\CatalogImportExport\Model\Import\Product + * @param Product * Param should go with rewritten getOptionEntity method. - * @return \Magento\CatalogImportExport\Model\Import\Product\Option|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\CatalogImportExport\Model\Import\Product\Option|MockObject */ private function _rewriteGetOptionEntityInImportProduct($importProduct) { @@ -1665,12 +1674,12 @@ private function _rewriteGetOptionEntityInImportProduct($importProduct) /** * @param array $methods * @param array $errorAggregatorMethods - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function createModelMockWithErrorAggregator(array $methods = [], array $errorAggregatorMethods = []) { $methods[] = 'getErrorAggregator'; - $importProduct = $this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product::class) + $importProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() ->setMethods($methods) ->getMock(); diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php new file mode 100644 index 0000000000000..d66a783c6720d --- /dev/null +++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/AddQuantityAndStockStatusFieldToCollection.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Ui\DataProvider\Product; + +use Magento\Framework\Data\Collection; +use Magento\Ui\DataProvider\AddFieldToCollectionInterface; + +/** + * Add quantity_and_stock_status field to collection + */ +class AddQuantityAndStockStatusFieldToCollection implements AddFieldToCollectionInterface +{ + /** + * @inheritdoc + */ + public function addField(Collection $collection, $field, $alias = null) + { + $collection->joinField( + 'quantity_and_stock_status', + 'cataloginventory_stock_item', + 'is_in_stock', + 'product_id=entity_id', + '{{table}}.stock_id=1', + 'left' + ); + } +} diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml index 803a6dae492a0..601f7ef61973b 100644 --- a/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml +++ b/app/code/Magento/CatalogInventory/etc/adminhtml/di.xml @@ -23,6 +23,7 @@ <arguments> <argument name="addFieldStrategies" xsi:type="array"> <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFieldToCollection</item> + <item name="quantity_and_stock_status" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityAndStockStatusFieldToCollection</item> </argument> <argument name="addFilterStrategies" xsi:type="array"> <item name="qty" xsi:type="object">Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityFilterToCollection</item> diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index 535e91ba30f52..8c47865da6aa7 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -44,7 +44,7 @@ </type> <type name="Magento\CatalogInventory\Observer\UpdateItemsStockUponConfigChangeObserver"> <arguments> - <argument name="resourceStock" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Proxy</argument> + <argument name="resourceStockItem" xsi:type="object">Magento\CatalogInventory\Model\ResourceModel\Stock\Item\Proxy</argument> </arguments> </type> <type name="Magento\Catalog\Model\Layer"> diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php index fc2056e83ec70..12334a2a773cb 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php @@ -13,7 +13,12 @@ use Magento\Store\Model\Store; use Magento\UrlRewrite\Model\UrlPersistInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +/** + * Observer to assign the products to website. + */ class ProductToWebsiteChangeObserver implements ObserverInterface { /** @@ -58,23 +63,30 @@ public function __construct( * Generate urls for UrlRewrite and save it in storage * * @param \Magento\Framework\Event\Observer $observer + * @throws NoSuchEntityException + * @throws UrlAlreadyExistsException * @return void */ public function execute(\Magento\Framework\Event\Observer $observer) { foreach ($observer->getEvent()->getProducts() as $productId) { + $storeId = $this->request->getParam('store_id', Store::DEFAULT_STORE_ID); + $product = $this->productRepository->getById( $productId, false, - $this->request->getParam('store_id', Store::DEFAULT_STORE_ID) + $storeId ); - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - ]); - if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); + if (!empty($this->productUrlRewriteGenerator->generate($product))) { + $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/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml new file mode 100644 index 0000000000000..d52395342c092 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -0,0 +1,71 @@ +<?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="AdminUrlForProductRewrittenCorrectlyTest"> + <annotations> + <features value="CatalogUrlRewrite"/> + <stories value="Url rewrites for products"/> + <title value="Check that URL for product rewritten correctly"/> + <description value="Check that URL for product rewritten correctly"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13913"/> + <useCaseId value="MAGETWO-73534"/> + <group value="catalog"/> + <group value="catalogUrlRewrite"/> + </annotations> + <before> + <!--Create product--> + <createData entity="_defaultCategory" stepKey="category"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Open Created product--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="openProductEditPage"/> + <!--Switch to Default Store view--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="selectDefaultStoreView"> + <argument name="storeViewName" value="_defaultStore"/> + </actionGroup> + + <!--Set use default url--> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSearchEngineOptimizationTab"/> + <waitForElementVisible selector="{{AdminProductSEOSection.useDefaultUrl}}" time="30" stepKey="waitForUseDefaultUrlCheckbox"/> + <uncheckOption selector="{{AdminProductSEOSection.useDefaultUrl}}" stepKey="uncheckUseDefaultUrlCheckbox"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="$$createProduct.custom_attributes[url_key]$$-updated" stepKey="changeUrlKey"/> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + + <!--Select product and go toUpdate Attribute page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductsGrid"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterGridBySku"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="selectFilteredProduct"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickActionDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickBulkUpdateAttributes"/> + <waitForPageLoad stepKey="waitForUpdateAttributesPageLoad"/> + <seeInCurrentUrl url="{{AdminProductUpdateAttributesPage.url}}" stepKey="seeInUrlAttributeUpdatePage"/> + <click selector="{{AdminUpdateAttributesWebsiteSection.website}}" stepKey="openWebsitesTab"/> + <waitForAjaxLoad stepKey="waitForLoadWebSiteTab"/> + <click selector="{{AdminUpdateAttributesWebsiteSection.addProductToWebsite}}" stepKey="checkAddProductToWebsiteCheckbox"/> + <click selector="{{AdminUpdateAttributesHeaderSection.saveButton}}" stepKey="clickSave"/> + <see selector="{{AdminMessagesSection.success}}" userInput="A total of 1 record(s) were updated." stepKey="seeSaveSuccessMessage"/> + <!--Got to Store front product page and check url--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$-updated)}}" stepKey="navigateToSimpleProductPage"/> + <seeInCurrentUrl url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$-updated)}}" stepKey="seeProductNewUrl"/> + <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createProduct.sku$$" stepKey="seeCorrectSku"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductToWebsiteChangeObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductToWebsiteChangeObserverTest.php new file mode 100644 index 0000000000000..f383c949b4295 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductToWebsiteChangeObserverTest.php @@ -0,0 +1,193 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogUrlRewrite\Observer\ProductToWebsiteChangeObserver; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use Magento\Store\Model\Store; + +/** + * Test for ProductToWebsiteChangeObserver + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ProductToWebsiteChangeObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $productRepository; + + /** + * @var ProductUrlRewriteGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + private $productUrlRewriteGenerator; + + /** + * @var UrlPersistInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $urlPersist; + + /** + * @var Event|\PHPUnit_Framework_MockObject_MockObject + */ + private $event; + + /** + * @var Observer|\PHPUnit_Framework_MockObject_MockObject + */ + private $observer; + + /** + * @var Product|\PHPUnit_Framework_MockObject_MockObject + */ + private $product; + + /** + * @var RequestInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $request; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ProductToWebsiteChangeObserver + */ + private $model; + + /** + * @var int + */ + private $productId; + + /** + * @inheritDoc + */ + protected function setUp() + { + $this->productId = 3; + + $this->urlPersist = $this->getMockBuilder(UrlPersistInterface::class) + ->setMethods(['deleteByData', 'replace']) + ->getMockForAbstractClass(); + $this->productRepository = $this->getMockBuilder(ProductRepositoryInterface::class) + ->setMethods(['getById']) + ->getMockForAbstractClass(); + $this->product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getId', 'getVisibility']) + ->getMock(); + $this->product->expects($this->any()) + ->method('getId') + ->willReturn($this->productId); + $this->productRepository->expects($this->any()) + ->method('getById') + ->with($this->productId, false, Store::DEFAULT_STORE_ID) + ->willReturn($this->product); + $this->productUrlRewriteGenerator = $this->getMockBuilder(ProductUrlRewriteGenerator::class) + ->disableOriginalConstructor() + ->setMethods(['generate']) + ->getMock(); + $this->event = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getProducts']) + ->getMock(); + $this->event->expects($this->any()) + ->method('getProducts') + ->willReturn([$this->productId]); + $this->observer = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->setMethods(['getEvent']) + ->getMock(); + $this->observer->expects($this->any()) + ->method('getEvent') + ->willReturn($this->event); + $this->request = $this->getMockBuilder(RequestInterface::class) + ->setMethods(['getParam']) + ->getMockForAbstractClass(); + $this->request->expects($this->any()) + ->method('getParam') + ->with('store_id', Store::DEFAULT_STORE_ID) + ->willReturn(Store::DEFAULT_STORE_ID); + + $this->objectManager = new ObjectManager($this); + $this->model = $this->objectManager->getObject( + ProductToWebsiteChangeObserver::class, + [ + 'productUrlRewriteGenerator' => $this->productUrlRewriteGenerator, + 'urlPersist' => $this->urlPersist, + 'productRepository' => $this->productRepository, + 'request' => $this->request + ] + ); + } + + /** + * @param array $urlRewriteGeneratorResult + * @param int $numberDeleteByData + * @param int $productVisibility + * @param int $numberReplace + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException + * @dataProvider executeDataProvider + */ + public function testExecute( + array $urlRewriteGeneratorResult, + int $numberDeleteByData, + int $productVisibility, + int $numberReplace + ) { + $this->productUrlRewriteGenerator->expects($this->any()) + ->method('generate') + ->willReturn($urlRewriteGeneratorResult); + $this->urlPersist->expects($this->exactly($numberDeleteByData)) + ->method('deleteByData') + ->with( + [ + UrlRewrite::ENTITY_ID => $this->productId, + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => Store::DEFAULT_STORE_ID + ] + ); + $this->product->expects($this->any()) + ->method('getVisibility') + ->willReturn($productVisibility); + $this->urlPersist->expects($this->exactly($numberReplace)) + ->method('replace') + ->with($urlRewriteGeneratorResult); + + $this->model->execute($this->observer); + } + + /** + * Data provider for testExecute + * + * @return array + */ + public function executeDataProvider(): array + { + return [ + [[], 0, Visibility::VISIBILITY_NOT_VISIBLE, 0], + [['someRewrite'], 1, Visibility::VISIBILITY_NOT_VISIBLE, 0], + [['someRewrite'], 1, Visibility::VISIBILITY_BOTH, 1], + ]; + } +} diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index cb462ada0fc91..41f5123f3b772 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -11,6 +11,10 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Widget\Block\BlockInterface; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\View\LayoutFactory; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\ActionInterface; /** * Catalog Products List widget block @@ -94,6 +98,21 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem */ private $json; + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * @var \Magento\Framework\Url\EncoderInterface + */ + private $urlEncoder; + + /** + * @var \Magento\Framework\View\Element\RendererList + */ + private $rendererListBlock; + /** * @param \Magento\Catalog\Block\Product\Context $context * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory @@ -104,6 +123,10 @@ class ProductsList extends \Magento\Catalog\Block\Product\AbstractProduct implem * @param \Magento\Widget\Helper\Conditions $conditionsHelper * @param array $data * @param Json|null $json + * @param LayoutFactory|null $layoutFactory + * @param \Magento\Framework\Url\EncoderInterface|null $urlEncoder + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Catalog\Block\Product\Context $context, @@ -114,7 +137,9 @@ public function __construct( \Magento\CatalogWidget\Model\Rule $rule, \Magento\Widget\Helper\Conditions $conditionsHelper, array $data = [], - Json $json = null + Json $json = null, + LayoutFactory $layoutFactory = null, + EncoderInterface $urlEncoder = null ) { $this->productCollectionFactory = $productCollectionFactory; $this->catalogProductVisibility = $catalogProductVisibility; @@ -123,6 +148,8 @@ public function __construct( $this->rule = $rule; $this->conditionsHelper = $conditionsHelper; $this->json = $json ?: ObjectManager::getInstance()->get(Json::class); + $this->layoutFactory = $layoutFactory ?: ObjectManager::getInstance()->get(LayoutFactory::class); + $this->urlEncoder = $urlEncoder ?: ObjectManager::getInstance()->get(EncoderInterface::class); parent::__construct( $context, $data @@ -151,6 +178,7 @@ protected function _construct() * Get key pieces for caching block content * * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getCacheKeyInfo() { @@ -210,6 +238,43 @@ public function getProductPriceHtml( return $price; } + /** + * @inheritdoc + */ + protected function getDetailsRendererList() + { + if (empty($this->rendererListBlock)) { + /** @var $layout \Magento\Framework\View\LayoutInterface */ + $layout = $this->layoutFactory->create(['cacheable' => false]); + $layout->getUpdate()->addHandle('catalog_widget_product_list')->load(); + $layout->generateXml(); + $layout->generateElements(); + + $this->rendererListBlock = $layout->getBlock('category.product.type.widget.details.renderers'); + } + + return $this->rendererListBlock; + } + + /** + * Get post parameters. + * + * @param Product $product + * @return array + */ + public function getAddToCartPostParams(Product $product): array + { + $url = $this->getAddToCartUrl($product); + + return [ + 'action' => $url, + 'data' => [ + 'product' => $product->getEntityId(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($url), + ] + ]; + } + /** * {@inheritdoc} */ @@ -223,6 +288,7 @@ protected function _beforeToHtml() * Prepare and return product collection * * @return \Magento\Catalog\Model\ResourceModel\Product\Collection + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function createCollection() { @@ -400,6 +466,24 @@ private function getPriceCurrency() } /** + * @inheritdoc + */ + public function getAddToCartUrl($product, $additional = []) + { + $requestingPageUrl = $this->getRequest()->getParam('requesting_page_url'); + + if (!empty($requestingPageUrl)) { + $additional['useUencPlaceholder'] = true; + $url = parent::getAddToCartUrl($product, $additional); + return str_replace('%25uenc%25', $this->urlEncoder->encode($requestingPageUrl), $url); + } + + return parent::getAddToCartUrl($product, $additional); + } + + /** + * Get widget block name + * * @return string */ private function getWidgetPagerBlockName() diff --git a/app/code/Magento/CatalogWidget/Setup/UpgradeData.php b/app/code/Magento/CatalogWidget/Setup/UpgradeData.php new file mode 100644 index 0000000000000..5ebdbe2390d51 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Setup/UpgradeData.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogWidget\Setup; + +use Magento\CatalogWidget\Block\Product\ProductsList; +use Magento\CatalogWidget\Model\Rule\Condition\Product as ConditionProduct; +use Magento\Framework\Serialize\Serializer\Json as Serializer; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\UpgradeDataInterface; + +/** + * Upgrade data for CatalogWidget module. + */ +class UpgradeData implements UpgradeDataInterface +{ + /** + * @var Serializer + */ + private $serializer; + + /** + * @param Serializer $serializer + */ + public function __construct( + Serializer $serializer + ) { + $this->serializer = $serializer; + } + + /** + * @inheritdoc + */ + public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + if (version_compare($context->getVersion(), '2.0.1', '<')) { + $this->replaceIsWithIsOneOf($setup); + } + } + + /** + * Replace 'is' condition with 'is one of' in database. + * + * If 'is' product list condition is used with multiple skus it should be replaced by 'is one of' condition. + * + * @param ModuleDataSetupInterface $setup + */ + private function replaceIsWithIsOneOf(ModuleDataSetupInterface $setup) + { + $tableName = $setup->getTable('widget_instance'); + $connection = $setup->getConnection(); + $select = $connection->select() + ->from( + $tableName, + [ + 'instance_id', + 'widget_parameters', + ] + )->where('instance_type = ? ', ProductsList::class); + + $result = $setup->getConnection()->fetchAll($select); + + if ($result) { + $updatedData = $this->updateWidgetData($result); + + $connection->insertOnDuplicate( + $tableName, + $updatedData + ); + } + } + + /** + * Replace 'is' condition with 'is one of' in widget parameters. + * + * @param array $result + * @return array + */ + private function updateWidgetData(array $result): array + { + return array_map( + function ($widgetData) { + $widgetParameters = $this->serializer->unserialize($widgetData['widget_parameters']); + foreach ($widgetParameters['conditions'] as &$condition) { + if (ConditionProduct::class === $condition['type'] && + 'sku' === $condition['attribute'] && + '==' === $condition['operator']) { + $condition['operator'] = '()'; + } + } + $widgetData['widget_parameters'] = $this->serializer->serialize($widgetParameters); + + return $widgetData; + }, + $result + ); + } +} diff --git a/app/code/Magento/CatalogWidget/etc/module.xml b/app/code/Magento/CatalogWidget/etc/module.xml index 8954f11f954f7..1f2d84bef2d6b 100644 --- a/app/code/Magento/CatalogWidget/etc/module.xml +++ b/app/code/Magento/CatalogWidget/etc/module.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_CatalogWidget" setup_version="2.0.0"> + <module name="Magento_CatalogWidget" setup_version="2.0.1"> <sequence> <module name="Magento_Catalog"/> <module name="Magento_Widget"/> diff --git a/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml new file mode 100644 index 0000000000000..4fe7af7f34683 --- /dev/null +++ b/app/code/Magento/CatalogWidget/view/frontend/layout/catalog_widget_product_list.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <block class="Magento\Framework\View\Element\RendererList" name="category.product.type.widget.details.renderers"> + <block class="Magento\Framework\View\Element\Template" name="category.product.type.details.renderers.default" as="default"/> + </block> + </body> +</page> diff --git a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml index 574cbe1107e88..0217838b7fd8b 100644 --- a/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml +++ b/app/code/Magento/CatalogWidget/view/frontend/templates/product/widget/content/grid.phtml @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +use Magento\Framework\App\Action\Action; // @codingStandardsIgnoreFile @@ -48,57 +49,67 @@ <?= $block->escapeHtml($_item->getName()) ?> </a> </strong> - <?php - echo $block->getProductPriceHtml($_item, $type); - ?> - <?php if ($templateType): ?> <?= $block->getReviewsSummaryHtml($_item, $templateType) ?> <?php endif; ?> + <?= $block->getProductPriceHtml($_item, $type) ?> + + <?= $block->getProductDetailsHtml($_item) ?> + <?php if ($showWishlist || $showCompare || $showCart): ?> - <div class="product-item-actions"> - <?php if ($showCart): ?> - <div class="actions-primary"> - <?php if ($_item->isSaleable()): ?> - <?php if ($_item->getTypeInstance()->hasRequiredOptions($_item)): ?> - <button class="action tocart primary" data-mage-init='{"redirectUrl":{"url":"<?= $block->escapeUrl($block->getAddToCartUrl($_item)) ?>"}}' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> - <?php else: ?> - <?php - $postDataHelper = $this->helper('Magento\Framework\Data\Helper\PostHelper'); - $postData = $postDataHelper->getPostData($block->getAddToCartUrl($_item), ['product' => $_item->getEntityId()]) - ?> - <button class="action tocart primary" data-post='<?= /* @noEscape */ $postData ?>' type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>"> - <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> - </button> + <div class="product-item-inner"> + <div class="product-item-actions"> + <?php if ($showCart): ?> + <div class="actions-primary"> + <?php if ($_item->isSaleable()): ?> + <?php $postParams = $block->getAddToCartPostParams($_item); ?> + <form data-role="tocart-form" + data-product-sku="<?= $block->escapeHtml($_item->getSku()) ?>" + action="<?= /* @NoEscape */ + $postParams['action'] ?>" method="post"> + <input type="hidden" name="product" + value="<?= /* @escapeNotVerified */ + $postParams['data']['product'] ?>"> + <input type="hidden" name="<?= /* @escapeNotVerified */ + Action::PARAM_NAME_URL_ENCODED ?>" + value="<?= /* @escapeNotVerified */ + $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> + <?= $block->getBlockHtml('formkey') ?> + <button type="submit" + title="<?= $block->escapeHtml(__('Add to Cart')) ?>" + class="action tocart primary"> + <span><?= /* @escapeNotVerified */ + __('Add to Cart') ?></span> + </button> + </form> + <?php else: ?> + + <?php if ($_item->getIsSalable()): ?> + <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> + <?php else: ?> + <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php endif; ?> + <?php endif; ?> + </div> + <?php endif; ?> + <?php if ($showWishlist || $showCompare): ?> + <div class="actions-secondary" data-role="add-to-links"> + <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> + <a href="#" + data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> + <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> + </a> <?php endif; ?> - <?php else: ?> - <?php if ($_item->getIsSalable()): ?> - <div class="stock available"><span><?= $block->escapeHtml(__('In stock')) ?></span></div> - <?php else: ?> - <div class="stock unavailable"><span><?= $block->escapeHtml(__('Out of stock')) ?></span></div> + <?php if ($block->getAddToCompareUrl() && $showCompare): ?> + <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> + <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> + <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> + </a> <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($showWishlist || $showCompare): ?> - <div class="actions-secondary" data-role="add-to-links"> - <?php if ($this->helper('Magento\Wishlist\Helper\Data')->isAllow() && $showWishlist): ?> - <a href="#" - data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($_item) ?>' class="action towishlist" data-action="add-to-wishlist" title="<?= $block->escapeHtmlAttr(__('Add to Wish List')) ?>"> - <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> - </a> - <?php endif; ?> - <?php if ($block->getAddToCompareUrl() && $showCompare): ?> - <?php $compareHelper = $this->helper('Magento\Catalog\Helper\Product\Compare');?> - <a href="#" class="action tocompare" data-post='<?= /* @noEscape */ $compareHelper->getPostDataParams($_item) ?>' title="<?= $block->escapeHtmlAttr(__('Add to Compare')) ?>"> - <span><?= $block->escapeHtml(__('Add to Compare')) ?></span> - </a> - <?php endif; ?> - </div> - <?php endif; ?> + </div> + <?php endif; ?> + </div> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 5c237eecf0a9f..5e3234e9f4cc8 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -67,7 +67,7 @@ public function __construct( } /** - * Returns minicart config + * Returns minicart config. * * @return array */ @@ -82,7 +82,8 @@ public function getConfig() 'baseUrl' => $this->getBaseUrl(), 'minicartMaxItemsVisible' => $this->getMiniCartMaxItemsCount(), 'websiteId' => $this->_storeManager->getStore()->getWebsiteId(), - 'maxItemsToDisplay' => $this->getMaxItemsToDisplay() + 'maxItemsToDisplay' => $this->getMaxItemsToDisplay(), + 'storeId' => $this->_storeManager->getStore()->getId(), ]; } @@ -132,6 +133,7 @@ public function getShoppingCartUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getUpdateItemQtyUrl() { @@ -143,6 +145,7 @@ public function getUpdateItemQtyUrl() * * @return string * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ public function getRemoveItemUrl() { diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php index f47e514948d69..c5d4d68b06225 100644 --- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php +++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php @@ -6,8 +6,11 @@ namespace Magento\Checkout\Block\Checkout; use Magento\Checkout\Helper\Data; +use Magento\Customer\Model\AttributeMetadataDataProvider; +use Magento\Customer\Model\Options; use Magento\Framework\App\ObjectManager; use Magento\Store\Api\StoreResolverInterface; +use Magento\Ui\Component\Form\AttributeMapper; /** * Class LayoutProcessor @@ -15,12 +18,12 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcessorInterface { /** - * @var \Magento\Customer\Model\AttributeMetadataDataProvider + * @var AttributeMetadataDataProvider */ private $attributeMetadataDataProvider; /** - * @var \Magento\Ui\Component\Form\AttributeMapper + * @var AttributeMapper */ protected $attributeMapper; @@ -30,7 +33,7 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso protected $merger; /** - * @var \Magento\Customer\Model\Options + * @var Options */ private $options; @@ -50,30 +53,21 @@ class LayoutProcessor implements \Magento\Checkout\Block\Checkout\LayoutProcesso private $shippingConfig; /** - * @param \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider - * @param \Magento\Ui\Component\Form\AttributeMapper $attributeMapper + * @param AttributeMetadataDataProvider $attributeMetadataDataProvider + * @param AttributeMapper $attributeMapper * @param AttributeMerger $merger + * @param Options|null $options */ public function __construct( - \Magento\Customer\Model\AttributeMetadataDataProvider $attributeMetadataDataProvider, - \Magento\Ui\Component\Form\AttributeMapper $attributeMapper, - AttributeMerger $merger + AttributeMetadataDataProvider $attributeMetadataDataProvider, + AttributeMapper $attributeMapper, + AttributeMerger $merger, + Options $options = null ) { $this->attributeMetadataDataProvider = $attributeMetadataDataProvider; $this->attributeMapper = $attributeMapper; $this->merger = $merger; - } - - /** - * @deprecated 100.0.11 - * @return \Magento\Customer\Model\Options - */ - private function getOptions() - { - if (!is_object($this->options)) { - $this->options = ObjectManager::getInstance()->get(\Magento\Customer\Model\Options::class); - } - return $this->options; + $this->options = $options ?? ObjectManager::getInstance()->get(Options::class); } /** @@ -143,8 +137,8 @@ private function convertElementsToSelect($elements, $attributesToConvert) public function process($jsLayout) { $attributesToConvert = [ - 'prefix' => [$this->getOptions(), 'getNamePrefixOptions'], - 'suffix' => [$this->getOptions(), 'getNameSuffixOptions'], + 'prefix' => [$this->options, 'getNamePrefixOptions'], + 'suffix' => [$this->options, 'getNameSuffixOptions'], ]; $elements = $this->getAddressAttributes(); diff --git a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php index 8654bdbde5893..1876d3ca37d94 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php +++ b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php @@ -41,6 +41,8 @@ public function execute() } } $this->cart->save(); + } else { + $this->messageManager->addErrorMessage(__('Please select at least one product to add to cart')); } return $this->_goBack(); } diff --git a/app/code/Magento/Checkout/CustomerData/Cart.php b/app/code/Magento/Checkout/CustomerData/Cart.php index 01e91d75c02d9..9154e9c99478e 100644 --- a/app/code/Magento/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Checkout/CustomerData/Cart.php @@ -98,7 +98,8 @@ public function getSectionData() 'items' => $this->getRecentItems(), 'extra_actions' => $this->layout->createBlock(\Magento\Catalog\Block\ShortcutButtons::class)->toHtml(), 'isGuestCheckoutAllowed' => $this->isGuestCheckoutAllowed(), - 'website_id' => $this->getQuote()->getStore()->getWebsiteId() + 'website_id' => $this->getQuote()->getStore()->getWebsiteId(), + 'storeId' => $this->getQuote()->getStore()->getStoreId(), ]; } diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php index c0ba9616754bb..0eb59fc70d92f 100644 --- a/app/code/Magento/Checkout/Model/Cart.php +++ b/app/code/Magento/Checkout/Model/Cart.php @@ -15,6 +15,7 @@ * Shopping cart model * * @api + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @deprecated 100.1.0 Use \Magento\Quote\Model\Quote instead */ @@ -354,22 +355,10 @@ protected function _getProductRequest($requestInfo) public function addProduct($productInfo, $requestInfo = null) { $product = $this->_getProduct($productInfo); - $request = $this->_getProductRequest($requestInfo); $productId = $product->getId(); if ($productId) { - $stockItem = $this->stockRegistry->getStockItem($productId, $product->getStore()->getWebsiteId()); - $minimumQty = $stockItem->getMinSaleQty(); - //If product quantity is not specified in request and there is set minimal qty for it - if ($minimumQty - && $minimumQty > 0 - && !$request->getQty() - ) { - $request->setQty($minimumQty); - } - } - - if ($productId) { + $request = $this->getQtyRequest($product, $requestInfo); try { $result = $this->getQuote()->addProduct($product, $request); } catch (\Magento\Framework\Exception\LocalizedException $e) { @@ -425,8 +414,9 @@ public function addProductsByIds($productIds) } $product = $this->_getProduct($productId); if ($product->getId() && $product->isVisibleInCatalog()) { + $request = $this->getQtyRequest($product); try { - $this->getQuote()->addProduct($product); + $this->getQuote()->addProduct($product, $request); } catch (\Exception $e) { $allAdded = false; } @@ -747,4 +737,26 @@ private function getRequestInfoFilter() } return $this->requestInfoFilter; } + + /** + * Get request quantity + * + * @param Product $product + * @param \Magento\Framework\DataObject|int|array $request + * @return int|DataObject + */ + private function getQtyRequest($product, $request = 0) + { + $request = $this->_getProductRequest($request); + $stockItem = $this->stockRegistry->getStockItem($product->getId(), $product->getStore()->getWebsiteId()); + $minimumQty = $stockItem->getMinSaleQty(); + //If product quantity is not specified in request and there is set minimal qty for it + if ($minimumQty + && $minimumQty > 0 + && !$request->getQty() + ) { + $request->setQty($minimumQty); + } + return $request; + } } diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml index f79d59028c468..d20616b4384d1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <!-- Checkout select Check/Money Order payment --> <actionGroup name="CheckoutSelectCheckMoneyOrderPaymentActionGroup"> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> @@ -37,7 +37,7 @@ <argument name="orderNumberMessage"/> <argument name="emailYouMessage"/> </arguments> - <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{orderNumberMessage}}" stepKey="seeOrderNumber"/> <see selector="{{CheckoutSuccessMainSection.success}}" userInput="{{emailYouMessage}}" stepKey="seeEmailYou"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml index 7a5c5e1d15872..3390dc588b69b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontMiniCartActionGroup.xml @@ -15,7 +15,7 @@ </actionGroup> <actionGroup name="assertOneProductNameInMiniCart"> <arguments> - <argument name="productName"/> + <argument name="productName" type="string"/> </arguments> <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml index 8e043f85a0b95..115eb92c48268 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingGuestInfoSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="CheckoutShippingGuestInfoSection"> <element name="email" type="input" selector="#customer-email"/> <element name="firstName" type="input" selector="input[name=firstname]"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index 76c9e9f674e6a..b735abc665022 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -33,5 +33,9 @@ <element name="addCountry" type="select" selector="#shipping-new-address-form select[name='country_id']"/> <element name="addSaveButton" type="button" selector=".action.primary.action-save-address"/> <element name="editActiveAddress" type="button" selector="//div[@class='shipping-address-item selected-item']//span[text()='Edit']" timeout="30"/> + <element name="namePrefix" type="select" selector="select[name=prefix]"/> + <element name="namePrefixOption" type="text" selector="select[name=prefix] option[value='{{optionValue}}']" parameterized="true"/> + <element name="nameSuffix" type="selector" selector="[name='suffix']"/> + <element name="nameSuffixOption" type="text" selector="select[name='suffix'] option[value='{{optionValue}}']" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoCreatedByGuestTest.xml new file mode 100644 index 0000000000000..32cc254543bca --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoCreatedByGuestTest.xml @@ -0,0 +1,63 @@ +<?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="StorefrontCheckCustomerInfoCreatedByGuestTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check order customer information created by guest"/> + <title value="Check Order Customer Information Created By Guest"/> + <description value="Check customer information after placing the order as the guest who created an account"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13839"/> + <useCaseId value="MAGETWO-95182"/> + <group value="checkout"/> + <group value="customer"/> + <group value="sales"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> + <deleteData createDataKey="createCategory" stepKey="deleteCategory" /> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + <actionGroup ref="CustomerLogoutStorefrontActionGroup" stepKey="customerLogoutFromStorefront" /> + </after> + + <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="navigateToProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage" /> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <click selector="{{CheckoutSuccessRegisterSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.passwordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="typePassword"/> + <fillField selector="{{StorefrontCustomerCreateFormSection.confirmPasswordField}}" userInput="{{CustomerEntityOne.password}}" stepKey="typeConfirmationPassword"/> + <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickOnCreateAccount"/> + <see selector="{{StorefrontMessagesSection.successMessage}}" userInput="Thank you for registering" stepKey="verifyAccountCreated"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdmin"/> + <amOnPage url="{{AdminOrderDetailsPage.url('$grabOrderNumber')}}" stepKey="navigateToOrderPage"/> + <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminOrderDetailsInformationSection.customerName}}" stepKey="seeCustomerName"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml index c5269ca5d0b56..249c6dafae1a3 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml @@ -73,7 +73,7 @@ <click selector="{{OrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> <waitForPageLoad stepKey="waitForOrderPageLoad"/> <see selector="{{OrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeAdminOrderStatus"/> - <see selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="Guest" stepKey="seeAdminOrderGuest"/> + <see selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.fullname}}" stepKey="seeAdminOrderGuest"/> <see selector="{{OrderDetailsInformationSection.accountInformation}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeAdminOrderEmail"/> <see selector="{{OrderDetailsInformationSection.billingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderBillingAddress"/> <see selector="{{OrderDetailsInformationSection.shippingAddress}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="seeAdminOrderShippingAddress"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml new file mode 100644 index 0000000000000..9fee0302345d5 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml @@ -0,0 +1,90 @@ +<?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="StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest"> + <annotations> + <features value="Checkout"/> + <title value="Checking Product name with custom store views"/> + <description value="Checking Product name in Minicart and on Checkout page with custom store views"/> + <stories value="Checkout via Guest Checkout"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14944"/> + <useCaseId value="MAGETWO-95904"/> + <group value="checkout"/> + </annotations> + <before> + <!--Create a product--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!--Login as Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <!-- Create store view --> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + <after> + <!--Delete product--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <!--Delete store view--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <!--Logout from admin--> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Go to created product page--> + <amOnPage url="{{AdminProductEditPage.url($$createProduct.id$$)}}" stepKey="goToEditPage"/> + + <!--Switch to second store view and change the product name--> + <actionGroup ref="SwitchToTheNewStoreView" stepKey="switchToCustomStoreView"> + <argument name="storeViewName" value="customStore"/> + </actionGroup> + <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + + <!--Add product to cart--> + <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> + <actionGroup ref="addToCartFromStorefrontProductPage" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$$"/> + </actionGroup> + + <!--Check simple product in minicart--> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProductNameInMiniCart1"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!--Switch to second store view--> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreView"> + <argument name="storeView" value="customStore"/> + </actionGroup> + + <!--Check simple product in minicart--> + <actionGroup ref="assertOneProductNameInMiniCart" stepKey="assertProductNameInMiniCart2"> + <argument name="productName" value="$$createProduct.name$$-new"/> + </actionGroup> + + <!--Go to Shopping Cart--> + <actionGroup ref="clickViewAndEditCartFromMiniCart" stepKey="goToShoppingCartFromMinicart"/> + <seeElement selector="{{CheckoutCartProductSection.productLinkByName($$createProduct.name$$-new)}}" stepKey="assertProductName"/> + + <!--Proceed to checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutPage"/> + <conditionalClick selector="{{CheckoutOrderSummarySection.miniCartTab}}" dependentSelector="{{CheckoutOrderSummarySection.miniCartTab}}" visible="true" stepKey="clickItemsInCart"/> + <waitForElementVisible selector="{{CheckoutOrderSummarySection.productItemName}}" stepKey="waitForProduct"/> + <see selector="{{CheckoutOrderSummarySection.productItemName}}" userInput="$$createProduct.name$$-new" stepKey="seeProductNameAtCheckout"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php index 88751b899d7c9..015d8ccbe928f 100644 --- a/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Block/Cart/SidebarTest.php @@ -6,6 +6,8 @@ namespace Magento\Checkout\Test\Unit\Block\Cart; /** + * Unit tests for Magento\Checkout\Block\Cart\Sidebar. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class SidebarTest extends \PHPUnit\Framework\TestCase @@ -123,6 +125,11 @@ public function testGetTotalsHtml() $this->assertEquals($totalsHtml, $this->model->getTotalsHtml()); } + /** + * Unit test for getConfig method. + * + * @return void + */ public function testGetConfig() { $websiteId = 100; @@ -144,14 +151,15 @@ public function testGetConfig() 'baseUrl' => $baseUrl, 'minicartMaxItemsVisible' => 3, 'websiteId' => 100, - 'maxItemsToDisplay' => 8 + 'maxItemsToDisplay' => 8, + 'storeId' => null, ]; $valueMap = [ ['checkout/cart', [], $shoppingCartUrl], ['checkout', [], $checkoutUrl], ['checkout/sidebar/updateItemQty', ['_secure' => false], $updateItemQtyUrl], - ['checkout/sidebar/removeItem', ['_secure' => false], $removeItemUrl] + ['checkout/sidebar/removeItem', ['_secure' => false], $removeItemUrl], ]; $this->requestMock->expects($this->any()) @@ -161,7 +169,7 @@ public function testGetConfig() $this->urlBuilderMock->expects($this->exactly(4)) ->method('getUrl') ->willReturnMap($valueMap); - $this->storeManagerMock->expects($this->exactly(2))->method('getStore')->willReturn($storeMock); + $this->storeManagerMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeMock); $storeMock->expects($this->once())->method('getBaseUrl')->willReturn($baseUrl); $this->imageHelper->expects($this->once())->method('getFrame')->willReturn(false); diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php index 75e181cbabd08..9f718f00b4b9d 100644 --- a/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php +++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/CartTest.php @@ -7,6 +7,8 @@ namespace Magento\Checkout\Test\Unit\CustomerData; /** + * Unit tests for Magento\Checkout\CustomerData\Cart. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CartTest extends \PHPUnit\Framework\TestCase @@ -78,6 +80,11 @@ public function testIsGuestCheckoutAllowed() $this->assertTrue($this->model->isGuestCheckoutAllowed()); } + /** + * Unit test for getSectionData method. + * + * @return void + */ public function testGetSectionData() { $summaryQty = 100; @@ -113,7 +120,7 @@ public function testGetSectionData() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeMock); $productMock = $this->createPartialMock( \Magento\Catalog\Model\Product::class, @@ -162,6 +169,7 @@ public function testGetSectionData() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null, ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } @@ -199,7 +207,7 @@ public function testGetSectionDataWithCompositeProduct() $storeMock = $this->createPartialMock(\Magento\Store\Model\System\Store::class, ['getWebsiteId']); $storeMock->expects($this->once())->method('getWebsiteId')->willReturn($websiteId); - $quoteMock->expects($this->once())->method('getStore')->willReturn($storeMock); + $quoteMock->expects($this->atLeastOnce())->method('getStore')->willReturn($storeMock); $this->checkoutCartMock->expects($this->once())->method('getSummaryQty')->willReturn($summaryQty); $this->checkoutHelperMock->expects($this->once()) @@ -265,6 +273,7 @@ public function testGetSectionDataWithCompositeProduct() 'isGuestCheckoutAllowed' => 1, 'website_id' => $websiteId, 'subtotalAmount' => 200, + 'storeId' => null, ]; $this->assertEquals($expectedResult, $this->model->getSectionData()); } diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index 71dfd12bb4779..4ebd594a28562 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -49,7 +49,4 @@ </argument> </arguments> </type> - <type name="Magento\Quote\Model\Quote"> - <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> - </type> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 00bcd2a27005a..8f35fe9f37abf 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -96,4 +96,7 @@ </argument> </arguments> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> + </type> </config> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 1005c11e44d95..84ab9b13d8f3a 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -14,7 +14,8 @@ method="post" id="form-validate" data-mage-init='{"Magento_Checkout/js/action/update-shopping-cart": - {"validationURL" : "/checkout/cart/updateItemQty"} + {"validationURL" : "/checkout/cart/updateItemQty", + "updateCartActionContainer": "#update_cart_action_container"} }' class="form form-cart"> <?= $block->getBlockHtml('formkey') ?> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml index 1c0c221a550cd..67ac4a9335565 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/noItems.phtml @@ -13,3 +13,10 @@ $block->escapeUrl($block->getContinueShoppingUrl())) ?></p> <?= $block->getChildHtml('shopping.cart.table.after') ?> </div> +<script type="text/x-magento-init"> +{ + "*": { + "Magento_Checkout/js/empty-cart": {} + } +} +</script> \ No newline at end of file diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js index ce1527b3d72d6..1920bc4d7ac41 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/update-shopping-cart.js @@ -14,7 +14,8 @@ define([ $.widget('mage.updateShoppingCart', { options: { validationURL: '', - eventName: 'updateCartItemQty' + eventName: 'updateCartItemQty', + updateCartActionContainer: '' }, /** @inheritdoc */ @@ -31,7 +32,9 @@ define([ * @return {Boolean} */ onSubmit: function (event) { - if (!this.options.validationURL) { + var action = this.element.find(this.options.updateCartActionContainer).val(); + + if (!this.options.validationURL || action === 'empty_cart') { return true; } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js new file mode 100644 index 0000000000000..27d38697afe39 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Customer/js/customer-data' +], function (customerData) { + 'use strict'; + + customerData.reload(['cart'], false); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js index 6f9a1a46826da..d68b0682eb511 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js @@ -202,6 +202,13 @@ function ( } }, + /** + * Manage cancel button visibility + */ + canUseCancelBillingAddress: ko.computed(function () { + return quote.billingAddress() || lastSelectedBillingAddress; + }), + /** * Restore billing address */ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index a2f8c8c56ff33..5e29fa209a641 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -81,6 +81,7 @@ define([ maxItemsToDisplay: window.checkout.maxItemsToDisplay, cart: {}, + // jscs:disable requireCamelCaseOrUpperCaseIdentifiers /** * @override */ @@ -101,12 +102,16 @@ define([ self.isLoading(true); }); - if (cartData()['website_id'] !== window.checkout.websiteId) { + if (cartData().website_id !== window.checkout.websiteId || + cartData().store_id !== window.checkout.storeId + ) { customerData.reload(['cart'], false); } return this._super(); }, + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + isLoading: ko.observable(false), initSidebar: initSidebar, diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html index 5f735fbb4daa9..63edb5057b933 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address.html @@ -22,7 +22,7 @@ <button class="action action-update" type="button" data-bind="click: updateAddress"> <span data-bind="i18n: 'Update'"></span> </button> - <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit"> + <button class="action action-cancel" type="button" data-bind="click: cancelAddressEdit, visible: canUseCancelBillingAddress()"> <span data-bind="i18n: 'Cancel'"></span> </button> </div> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html index a448537d64e83..4b1a68624e547 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html +++ b/app/code/Magento/CheckoutAgreements/view/frontend/web/template/checkout/checkout-agreements.html @@ -5,17 +5,17 @@ */ --> <div data-role="checkout-agreements"> - <div class="checkout-agreements" data-bind="visible: isVisible"> + <div class="checkout-agreements fieldset" data-bind="visible: isVisible"> <!-- ko foreach: agreements --> <!-- ko if: ($parent.isAgreementRequired($data)) --> - <div class="checkout-agreement required"> + <div class="checkout-agreement field choice required"> <input type="checkbox" class="required-entry" data-bind="attr: { 'id': $parent.getCheckboxId($parentContext, agreementId), 'name': 'agreement[' + agreementId + ']', 'value': agreementId }"/> - <label data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> + <label class="label" data-bind="attr: {'for': $parent.getCheckboxId($parentContext, agreementId)}"> <button type="button" class="action action-show" data-bind="click: function(data, event) { return $parent.showContent(data, event) }" diff --git a/app/code/Magento/Cms/Model/ResourceModel/Block.php b/app/code/Magento/Cms/Model/ResourceModel/Block.php index 9aab54b02bc14..9b4bc5ec3ea11 100644 --- a/app/code/Magento/Cms/Model/ResourceModel/Block.php +++ b/app/code/Magento/Cms/Model/ResourceModel/Block.php @@ -95,9 +95,11 @@ protected function _beforeSave(AbstractModel $object) } /** + * Get block id. + * * @param AbstractModel $object * @param mixed $value - * @param null $field + * @param string $field * @return bool|int|string * @throws LocalizedException * @throws \Exception @@ -183,10 +185,12 @@ public function getIsUniqueBlockToStores(AbstractModel $object) $entityMetadata = $this->metadataPool->getMetadata(BlockInterface::class); $linkField = $entityMetadata->getLinkField(); - if ($this->_storeManager->isSingleStoreMode()) { - $stores = [Store::DEFAULT_STORE_ID]; - } else { - $stores = (array)$object->getData('store_id'); + $stores = (array)$object->getData('store_id'); + $isDefaultStore = $this->_storeManager->isSingleStoreMode() + || array_search(Store::DEFAULT_STORE_ID, $stores) !== false; + + if (!$isDefaultStore) { + $stores[] = Store::DEFAULT_STORE_ID; } $select = $this->getConnection()->select() @@ -196,8 +200,11 @@ public function getIsUniqueBlockToStores(AbstractModel $object) 'cb.' . $linkField . ' = cbs.' . $linkField, [] ) - ->where('cb.identifier = ?', $object->getData('identifier')) - ->where('cbs.store_id IN (?)', $stores); + ->where('cb.identifier = ?', $object->getData('identifier')); + + if (!$isDefaultStore) { + $select->where('cbs.store_id IN (?)', $stores); + } if ($object->getId()) { $select->where('cb.' . $entityMetadata->getIdentifierField() . ' <> ?', $object->getId()); @@ -236,6 +243,8 @@ public function lookupStoreIds($id) } /** + * Save an object. + * * @param AbstractModel $object * @return $this * @throws \Exception diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 2cd1647a1bf22..90dcf3dc8df78 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -270,7 +270,8 @@ public function getDirsCollection($path) $collection = $this->getCollection($path) ->setCollectDirs(true) ->setCollectFiles(false) - ->setCollectRecursively(false); + ->setCollectRecursively(false) + ->setOrder('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC); $conditions = $this->getConditionsForExcludeDirs(); diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsBlockActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsBlockActionGroup.xml new file mode 100644 index 0000000000000..597df165f61d1 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsBlockActionGroup.xml @@ -0,0 +1,52 @@ +<?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="FillCmsBlockForm"> + <arguments> + <argument name="title" type="string" defaultValue="{{DefaultCmsBlock.title}}"/> + <argument name="identifier" type="string" defaultValue="{{DefaultCmsBlock.identifier}}"/> + <argument name="store" type="string" defaultValue="[All Store View]"/> + <argument name="content" type="string" defaultValue="{{DefaultCmsBlock.content}}"/> + </arguments> + <fillField selector="{{AdminCmsBlockContentSection.title}}" userInput="{{title}}" stepKey="fillFieldTitle"/> + <fillField selector="{{AdminCmsBlockContentSection.identifier}}" userInput="{{identifier}}" stepKey="fillFieldIdentifier"/> + <selectOption selector="{{AdminCmsBlockContentSection.storeView}}" parameterArray="{{store}}" stepKey="selectStore" /> + <fillField selector="{{AdminCmsBlockContentSection.content}}" userInput="{{content}}" stepKey="fillContentField"/> + </actionGroup> + <actionGroup name="DeleteCmsBlockActionGroup"> + <arguments> + <argument name="cmsBlockIdentifier" type="string" defaultValue="{{DefaultCmsBlock.identifier}}"/> + </arguments> + <amOnPage url="{{AdminCmsBlockGridPage.url}}" stepKey="navigateToCmsBlockListingPage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersBeforeDelete"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openCmsBlockFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('identifier')}}" userInput="{{cmsBlockIdentifier}}" stepKey="fillFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters"/> + <click selector="{{CmsPagesPageActionsSection.select(cmsBlockIdentifier)}}" stepKey="clickOnSelect"/> + <click selector="{{CmsPagesPageActionsSection.delete(cmsBlockIdentifier)}}" stepKey="clickOnDelete"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.message}}" stepKey="waitForConfirmModal"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirm"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the block." stepKey="verifyBlockIsDeleted"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersAfterDelete"/> + </actionGroup> + <actionGroup name="NavigateToCreateCmsBlockActionGroup"> + <amOnPage url="{{AdminCmsBlockNewPage.url}}" stepKey="navigateToCreateCmsBlockPage"/> + </actionGroup> + <actionGroup name="SaveCmsBlockActionGroup"> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveBlockButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the block." stepKey="verifyMessage"/> + </actionGroup> + <actionGroup name="SaveCmsBlockWithErrorActionGroup" extends="SaveCmsBlockActionGroup"> + <arguments> + <argument name="errorMessage" type="string" defaultValue="A block identifier with the same properties already exists in the selected store."/> + </arguments> + <see selector="{{AdminMessagesSection.error}}" userInput="{{errorMessage}}" stepKey="verifyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewCmsPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewCmsPageActionGroup.xml new file mode 100644 index 0000000000000..667dc79d2d6b4 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewCmsPageActionGroup.xml @@ -0,0 +1,51 @@ +<?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="CreateNewCmsPageActionGroup"> + <arguments> + <argument name="cmsPage" defaultValue="_defaultCmsPage"/> + </arguments> + <amOnPage url="{{CmsNewPagePage.url}}" stepKey="amOnCMSNewPage"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{cmsPage.title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{cmsPage.identifier}}" stepKey="fillFieldUrlKey"/> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the page." stepKey="seeSaveSuccessMessage"/> + </actionGroup> + + <actionGroup name="CreateNewPageWithWidgetWithCategoryCondition" extends="CreateNewCmsPageActionGroup"> + <arguments> + <argument name="categoryId" type="string"/> + <argument name="conditionOperator" type="string" defaultValue="is"/> + <argument name="widgetType" type="string" defaultValue="Catalog Products List"/> + </arguments> + <click selector="{{CmsNewPagePageContentSection.header}}" after="fillFieldUrlKey" stepKey="clickExpandContent"/> + <click selector="{{CmsNewPagePageActionsSection.insertWidgetButton}}" after="clickExpandContent" stepKey="clickInsertWidgetButton"/> + + <selectOption selector="{{AdminNewWidgetSection.widgetTypeDropDown}}" userInput="{{widgetType}}" after="clickInsertWidgetButton" stepKey="selectCatalogProductListOption"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.addNewCondition}}" after="selectCatalogProductListOption" stepKey="waitForConditionsElementBecomeAvailable"/> + + <click selector="{{AdminNewWidgetSection.addNewCondition}}" after="waitForConditionsElementBecomeAvailable" stepKey="clickToAddCondition"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.selectCondition}}" after="clickToAddCondition" stepKey="waitForSelectBoxOpened"/> + + <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="Category" after="waitForSelectBoxOpened" stepKey="selectCategoryCondition"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.ruleParameter}}" after="selectCategoryCondition" stepKey="seeConditionsAdded"/> + + <click selector="{{AdminNewWidgetSection.conditionOperator}}" after="seeConditionsAdded" stepKey="clickToConditionIs"/> + <selectOption selector="{{AdminNewWidgetSection.conditionOperatorSelect('1')}}" after="clickToConditionIs" userInput="{{conditionOperator}}" stepKey="selectOperator"/> + + <click selector="{{AdminNewWidgetSection.ruleParameter}}" after="selectOperator" stepKey="clickAddConditionItem"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.setRuleParameter}}" after="clickAddConditionItem" stepKey="waitForConditionFieldOpened"/> + + <fillField selector="{{AdminNewWidgetSection.setRuleParameter}}" userInput="{{categoryId}}" after="waitForConditionFieldOpened" stepKey="setCategoryId"/> + <click selector="{{AdminNewWidgetSection.insertWidget}}" after="setCategoryId" stepKey="clickInsertWidget"/> + <waitForElementVisible selector="{{AdminMainActionsSection.save}}" after="clickInsertWidget" stepKey="waitForInsertWidgetSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml index 05e61ac86e166..fd20954dc7a9e 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml @@ -6,18 +6,22 @@ */ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="DeletePageByUrlKeyActionGroup"> <arguments> <argument name="urlKey" type="string"/> </arguments> <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnCMSPagesIndexPage"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <waitForPageLoad time="30" stepKey="waitForCmsPageListingLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersBeforeDelete"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openCmsPageFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('identifier')}}" userInput="{{urlKey}}" stepKey="fillFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFilters"/> <click selector="{{CmsPagesPageActionsSection.select(urlKey)}}" stepKey="clickSelect"/> <click selector="{{CmsPagesPageActionsSection.delete(urlKey)}}" stepKey="clickDelete"/> - <waitForElementVisible selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="waitForOkButtonToBeVisible"/> - <click selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="clickOkButton"/> - <waitForPageLoad stepKey="waitForPageLoad3"/> - <see userInput="The page has been deleted." stepKey="seeSuccessMessage"/> + <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForOkButtonToBeVisible"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickOkButton"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The page has been deleted." stepKey="seeSuccessMessage"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFiltersAfterDelete"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockNewPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockNewPage.xml new file mode 100644 index 0000000000000..2868d832ad762 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/AdminCmsBlockNewPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCmsBlockNewPage" url="/cms/block/new/" area="admin" module="Magento_Cms"> + <section name="AdminCmsBlockContentSection"/> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Page/StorefrontCmsPage.xml b/app/code/Magento/Cms/Test/Mftf/Page/StorefrontCmsPage.xml new file mode 100644 index 0000000000000..b2de3a225f8ce --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Page/StorefrontCmsPage.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="StorefrontCmsPage" url="/{{urlKey}}" area="storefront" module="Magento_Cms" parameterized="true"> + </page> +</pages> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml index 9614f13f9e3d3..20e55c49ec235 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminCmsBlockContentSection.xml @@ -11,5 +11,8 @@ <section name="AdminCmsBlockContentSection"> <element name="content" type="textarea" selector="#cms_block_form_content"/> <element name="insertWidgetButton" type="button" selector=".scalable.action-add-widget.plugin"/> + <element name="title" type="input" selector="input[name=title]"/> + <element name="identifier" type="input" selector="input[name=identifier]"/> + <element name="storeView" type="multiselect" selector="select[name=store_id]"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml index f60fced7d05d4..740650d6fdfa9 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePageActionsSection.xml @@ -11,5 +11,6 @@ <section name="CmsNewPagePageActionsSection"> <element name="savePage" type="button" selector="#save" timeout="30"/> <element name="saveAndContinueEdit" type="button" selector="#save_and_continue" timeout="10"/> + <element name="insertWidgetButton" type="button" selector=".scalable.action-add-widget.plugin" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckCreateStaticBlockOnDuplicateIdentifierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckCreateStaticBlockOnDuplicateIdentifierTest.xml new file mode 100644 index 0000000000000..ac1b68269740f --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckCreateStaticBlockOnDuplicateIdentifierTest.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="CheckCreateStaticBlockOnDuplicateIdentifierTest"> + <annotations> + <features value="Cms"/> + <stories value="Create CMS Block"/> + <title value="Check static blocks: ID should be unique per Store View"/> + <description value="Check static blocks: ID should be unique per Store View"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-13912"/> + <useCaseId value="MAGETWO-86215"/> + <group value="cms"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite"> + <argument name="newWebsiteName" value="{{SecondWebsite.name}}"/> + <argument name="websiteCode" value="{{SecondWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStoreGroup"> + <argument name="website" value="{{SecondWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="storeGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCmsBlockActionGroup" stepKey="deleteCMSBlockActionGroup"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{SecondWebsite.name}}"/> + </actionGroup> + <actionGroup ref="logout" stepKey="logout"/> + </after> + <actionGroup ref="NavigateToCreateCmsBlockActionGroup" stepKey="navigateToCreateCmsBlock"/> + <actionGroup ref="FillCmsBlockForm" stepKey="fillCmsBlockForm"/> + <actionGroup ref="SaveCmsBlockActionGroup" stepKey="saveCmsBlock"/> + <actionGroup ref="NavigateToCreateCmsBlockActionGroup" stepKey="navigateToCreateDuplicateCmsBlock"/> + <actionGroup ref="FillCmsBlockForm" stepKey="fillDuplicateCmsBlockForm"> + <argument name="store" value="[{{_defaultStore.name}},{{SecondStoreUnique.name}}]"/> + </actionGroup> + <actionGroup ref="SaveCmsBlockWithErrorActionGroup" stepKey="assertErrorMessageOnSave"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index 906a7d4fbc605..20c0b2075f5c3 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -414,6 +414,10 @@ protected function generalTestGetDirsCollection($path, $collectionArray = [], $e ->method('setCollectRecursively') ->with(false) ->willReturnSelf(); + $storageCollectionMock->expects($this->once()) + ->method('setOrder') + ->with('basename', \Magento\Framework\Data\Collection\Filesystem::SORT_ORDER_ASC) + ->willReturnSelf(); $storageCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator($collectionArray)); diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml index 9f886f6f1345e..793fc7d26cb4a 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml @@ -146,7 +146,6 @@ <editor> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> - <rule name="validate-xml-identifier" xsi:type="boolean">true</rule> </validation> <editorType>text</editorType> </editor> diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php index bee334596e990..f2bf3116af9e4 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Price.php @@ -7,14 +7,15 @@ */ namespace Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; + +/** + * Class Price for configurable product + */ class Price extends \Magento\Catalog\Model\Product\Type\Price { /** - * Get product final price - * - * @param float $qty - * @param \Magento\Catalog\Model\Product $product - * @return float + * @inheritdoc */ public function getFinalPrice($qty, $product) { @@ -22,7 +23,10 @@ public function getFinalPrice($qty, $product) return $product->getCalculatedFinalPrice(); } if ($product->getCustomOption('simple_product') && $product->getCustomOption('simple_product')->getProduct()) { - $finalPrice = parent::getFinalPrice($qty, $product->getCustomOption('simple_product')->getProduct()); + /** @var Product $simpleProduct */ + $simpleProduct = $product->getCustomOption('simple_product')->getProduct(); + $simpleProduct->setCustomerGroupId($product->getCustomerGroupId()); + $finalPrice = parent::getFinalPrice($qty, $simpleProduct); } else { $priceInfo = $product->getPriceInfo(); $finalPrice = $priceInfo->getPrice('final_price')->getAmount()->getValue(); @@ -35,7 +39,7 @@ public function getFinalPrice($qty, $product) } /** - * {@inheritdoc} + * @inheritdoc */ public function getPrice($product) { @@ -48,6 +52,7 @@ public function getPrice($product) } } } + return 0; } } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php new file mode 100644 index 0000000000000..8bdde2aeb0cff --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollector.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; + +/** + * Plugin for CommonTaxCollector to apply Tax Class ID from child item for configurable product + */ +class CommonTaxCollector +{ + /** + * Apply Tax Class ID from child item for configurable product + * + * @param \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject + * @param QuoteDetailsItemInterface $result + * @param QuoteDetailsItemInterfaceFactory $itemDataObjectFactory + * @param AbstractItem $item + * @return QuoteDetailsItemInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterMapItem( + \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector $subject, + QuoteDetailsItemInterface $result, + QuoteDetailsItemInterfaceFactory $itemDataObjectFactory, + AbstractItem $item + ) : QuoteDetailsItemInterface { + if ($item->getProduct()->getTypeId() === Configurable::TYPE_CODE && $item->getHasChildren()) { + $childItem = $item->getChildren()[0]; + $result->getTaxClassKey()->setValue($childItem->getProduct()->getTaxClassId()); + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Setup/InstallData.php b/app/code/Magento/ConfigurableProduct/Setup/InstallData.php index 1c26f159405dd..57cc287aa24aa 100644 --- a/app/code/Magento/ConfigurableProduct/Setup/InstallData.php +++ b/app/code/Magento/ConfigurableProduct/Setup/InstallData.php @@ -57,18 +57,24 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface 'color' ]; foreach ($attributes as $attributeCode) { - $relatedProductTypes = explode( - ',', - $eavSetup->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode, 'apply_to') - ); - if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { - $relatedProductTypes[] = Configurable::TYPE_CODE; - $eavSetup->updateAttribute( - \Magento\Catalog\Model\Product::ENTITY, - $attributeCode, - 'apply_to', - implode(',', $relatedProductTypes) + if ($attribute = $eavSetup->getAttribute( + \Magento\Catalog\Model\Product::ENTITY, + $attributeCode, + 'apply_to' + )) { + $relatedProductTypes = explode( + ',', + $attribute ); + if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { + $relatedProductTypes[] = Configurable::TYPE_CODE; + $eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + $attributeCode, + 'apply_to', + implode(',', $relatedProductTypes) + ); + } } } } diff --git a/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php b/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php index 1ff78a632c3bb..113839e57791d 100644 --- a/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php +++ b/app/code/Magento/ConfigurableProduct/Setup/UpgradeData.php @@ -47,16 +47,18 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface if (version_compare($context->getVersion(), '2.2.0') < 0) { $relatedProductTypes = $this->getRelatedProductTypes('tier_price', $eavSetup); - $key = array_search(Configurable::TYPE_CODE, $relatedProductTypes); - if ($key !== false) { - unset($relatedProductTypes[$key]); - $this->updateRelatedProductTypes('tier_price', $relatedProductTypes, $eavSetup); + if (!empty($relatedProductTypes)) { + $key = array_search(Configurable::TYPE_CODE, $relatedProductTypes); + if ($key !== false) { + unset($relatedProductTypes[$key]); + $this->updateRelatedProductTypes('tier_price', $relatedProductTypes, $eavSetup); + } } } if (version_compare($context->getVersion(), '2.2.1') < 0) { $relatedProductTypes = $this->getRelatedProductTypes('manufacturer', $eavSetup); - if (!in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { + if (!empty($relatedProductTypes) && !in_array(Configurable::TYPE_CODE, $relatedProductTypes)) { $relatedProductTypes[] = Configurable::TYPE_CODE; $this->updateRelatedProductTypes('manufacturer', $relatedProductTypes, $eavSetup); } @@ -78,10 +80,17 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface */ private function getRelatedProductTypes(string $attributeId, EavSetup $eavSetup) { - return explode( - ',', - $eavSetup->getAttribute(Product::ENTITY, $attributeId, 'apply_to') - ); + if ($attribute = $eavSetup->getAttribute( + Product::ENTITY, + $attributeId, + 'apply_to' + )) { + return explode( + ',', + $attribute + ); + } + return []; } /** diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml index e16ee43978a1e..6c47a24315c9a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminCreateConfigurableProductActionGroup.xml @@ -90,4 +90,41 @@ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened2"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> </actionGroup> + + <actionGroup name="AdminCreateConfigurableProductTwoAttributesWithOptionsActionGroup" extends="AdminCreateConfigurableProductActionGroup"> + <arguments> + <argument name="attributeOption" type="string"/> + <argument name="attributeOption1" type="string"/> + <argument name="configurableAttributeCode1" type="string"/> + <argument name="attribute1Option" type="string"/> + <argument name="attribute1Option1" type="string"/> + <argument name="attribute1Option2" type="string"/> + </arguments> + + <remove keyForRemoval="startEditAttrSet"/> + <remove keyForRemoval="searchForAttrSet"/> + <remove keyForRemoval="waitForLoad"/> + <remove keyForRemoval="selectAttrSetProd"/> + <remove keyForRemoval="saveEditedProductForProduct"/> + <remove keyForRemoval="clickClearFilters"/> + <remove keyForRemoval="clickFiltersExpand"/> + <remove keyForRemoval="fillFilter"/> + <remove keyForRemoval="clickSearch"/> + <remove keyForRemoval="clickAttributeColorCheckbox"/> + <remove keyForRemoval="clickOnSelectAllSecond"/> + + <fillField userInput="{{configurationsPrice}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{configurationsQty}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + + <!--Select attributes --> + <click selector="{{AdminCreateProductConfigurationsPanel.checkboxByName(configurableAttributeCode)}}" after="openConfigurationPanel" stepKey="selectAttribute"/> + <click selector="{{AdminCreateProductConfigurationsPanel.checkboxByName(configurableAttributeCode1)}}" after="selectAttribute" stepKey="selectAttribute1"/> + + <!--Select options--> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attributeOption)}}" after="clickNextButton" stepKey="selectAttributeOption"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attributeOption1)}}" after="selectAttributeOption" stepKey="selectAttributeOption1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attribute1Option)}}" after="selectAttributeOption1" stepKey="selectAttribute1Option"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attribute1Option1)}}" after="selectAttribute1Option" stepKey="selectAttribute1Option1"/> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeByName(attribute1Option2)}}" after="selectAttribute1Option1" stepKey="selectAttribute1Option2"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml index 429d535952f76..7a722959c9996 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminNewAttributePanelSection"> <element name="container" type="text" selector="#create_new_attribute"/> <element name="saveAttribute" type="button" selector="#save"/> @@ -21,5 +21,12 @@ <element name="deleteOption" type="button" selector="#delete_button_option_{{row}}" parameterized="true"/> <element name="attributeSelect" type="select" selector="product[{{var}}]" parameterized="true"/> <element name="attributeName" type="select" selector="//option[text()='{{var}}']" parameterized="true"/> + <element name="useInSearch" type="select" selector="#is_searchable"/> + <element name="visibleInAdvancedSearch" type="select" selector="#is_visible_in_advanced_search"/> + <element name="comparableOnStorefront" type="select" selector="#is_comparable"/> + <element name="useInLayeredNavigation" type="select" selector="#is_filterable"/> + <element name="visibleOnCatalogPagesOnStorefront" type="select" selector="#is_visible_on_front"/> + <element name="useInProductListing" type="select" selector="#used_in_product_listing"/> + <element name="storefrontPropertiesTab" type="button" selector="#front_fieldset-wrapper"/> </section> </sections> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php index 64b9b3776442a..0fc650a4113c6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/PriceTest.php @@ -6,22 +6,47 @@ namespace Magento\ConfigurableProduct\Test\Unit\Model\Product\Type\Configurable; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\Option; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price as ConfigurablePrice; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Pricing\Amount\AmountInterface; +use Magento\Framework\Pricing\Price\PriceInterface; +use Magento\Framework\Pricing\PriceInfo\Base as PriceInfoBase; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit_Framework_MockObject_MockObject as MockObject; class PriceTest extends \PHPUnit\Framework\TestCase { - /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price */ + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var ConfigurablePrice + */ protected $model; - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; + /** + * @var ManagerInterface|MockObject + */ + private $eventManagerMock; + /** + * @inheritdoc + */ protected function setUp() { $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->eventManagerMock = $this->createPartialMock( + ManagerInterface::class, + ['dispatch'] + ); $this->model = $this->objectManagerHelper->getObject( - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price::class + ConfigurablePrice::class, + ['eventManager' => $this->eventManagerMock] ); } @@ -29,29 +54,29 @@ public function testGetFinalPrice() { $finalPrice = 10; $qty = 1; - $configurableProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class) - ->disableOriginalConstructor() - ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice', '__wakeUp']) - ->getMock(); - $customOption = $this->getMockBuilder(\Magento\Catalog\Model\Product\Configuration\Item\Option::class) + + /** @var Product|MockObject $configurableProduct */ + $configurableProduct = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() - ->setMethods(['getProduct']) + ->setMethods(['getCustomOption', 'getPriceInfo', 'setFinalPrice']) ->getMock(); - $priceInfo = $this->getMockBuilder(\Magento\Framework\Pricing\PriceInfo\Base::class) + /** @var PriceInfoBase|MockObject $priceInfo */ + $priceInfo = $this->getMockBuilder(PriceInfoBase::class) ->disableOriginalConstructor() ->setMethods(['getPrice']) ->getMock(); - $price = $this->getMockBuilder(\Magento\Framework\Pricing\Price\PriceInterface::class) + /** @var PriceInterface|MockObject $price */ + $price = $this->getMockBuilder(PriceInterface::class) ->disableOriginalConstructor() ->getMock(); - $amount = $this->getMockBuilder(\Magento\Framework\Pricing\Amount\AmountInterface::class) + /** @var AmountInterface|MockObject $amount */ + $amount = $this->getMockBuilder(AmountInterface::class) ->disableOriginalConstructor() ->getMock(); $configurableProduct->expects($this->any()) ->method('getCustomOption') ->willReturnMap([['simple_product', false], ['option_ids', false]]); - $customOption->expects($this->never())->method('getProduct'); $configurableProduct->expects($this->once())->method('getPriceInfo')->willReturn($priceInfo); $priceInfo->expects($this->once())->method('getPrice')->with('final_price')->willReturn($price); $price->expects($this->once())->method('getAmount')->willReturn($amount); @@ -60,4 +85,60 @@ public function testGetFinalPrice() $this->assertEquals($finalPrice, $this->model->getFinalPrice($qty, $configurableProduct)); } + + public function testGetFinalPriceWithSimpleProduct() + { + $finalPrice = 10; + $qty = 1; + $customerGroupId = 1; + + /** @var Product|MockObject $configurableProduct */ + $configurableProduct = $this->createPartialMock( + Product::class, + ['getCustomOption', 'setFinalPrice', 'getCustomerGroupId'] + ); + /** @var Option|MockObject $customOption */ + $customOption = $this->createPartialMock( + Option::class, + ['getProduct'] + ); + /** @var Product|MockObject $simpleProduct */ + $simpleProduct = $this->createPartialMock( + Product::class, + ['setCustomerGroupId', 'setFinalPrice', 'getPrice', 'getTierPrice', 'getData', 'getCustomOption'] + ); + + $configurableProduct->method('getCustomOption') + ->willReturnMap([ + ['simple_product', $customOption], + ['option_ids', false] + ]); + $configurableProduct->method('getCustomerGroupId')->willReturn($customerGroupId); + $configurableProduct->expects($this->atLeastOnce()) + ->method('setFinalPrice') + ->with($finalPrice) + ->willReturnSelf(); + $customOption->method('getProduct')->willReturn($simpleProduct); + $simpleProduct->expects($this->atLeastOnce()) + ->method('setCustomerGroupId') + ->with($customerGroupId) + ->willReturnSelf(); + $simpleProduct->method('getPrice')->willReturn($finalPrice); + $simpleProduct->method('getTierPrice')->with($qty)->willReturn($finalPrice); + $simpleProduct->expects($this->atLeastOnce()) + ->method('setFinalPrice') + ->with($finalPrice) + ->willReturnSelf(); + $simpleProduct->method('getData')->with('final_price')->willReturn($finalPrice); + $simpleProduct->method('getCustomOption')->with('option_ids')->willReturn(false); + $this->eventManagerMock->expects($this->once()) + ->method('dispatch') + ->with('catalog_product_get_final_price', ['product' => $simpleProduct, 'qty' => $qty]); + + $this->assertEquals( + $finalPrice, + $this->model->getFinalPrice($qty, $configurableProduct), + 'The final price calculation is wrong' + ); + } } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php new file mode 100644 index 0000000000000..1a5c6c0003bfa --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Tax/Model/Sales/Total/Quote/CommonTaxCollectorTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Tax\Model\Sales\Total\Quote; + +use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector as CommonTaxCollectorPlugin; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\Tax\Api\Data\QuoteDetailsItemInterface; +use Magento\Tax\Api\Data\QuoteDetailsItemInterfaceFactory; +use Magento\Tax\Api\Data\TaxClassKeyInterface; +use Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Test for CommonTaxCollector plugin + */ +class CommonTaxCollectorTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var CommonTaxCollectorPlugin + */ + private $commonTaxCollectorPlugin; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->commonTaxCollectorPlugin = $this->objectManager->getObject(CommonTaxCollectorPlugin::class); + } + + /** + * Test to apply Tax Class Id from child item for configurable product + */ + public function testAfterMapItem() + { + $childTaxClassId = 10; + + /** @var Product|MockObject $childProductMock */ + $childProductMock = $this->createPartialMock( + Product::class, + ['getTaxClassId'] + ); + $childProductMock->method('getTaxClassId')->willReturn($childTaxClassId); + /* @var AbstractItem|MockObject $quoteItemMock */ + $childQuoteItemMock = $this->createMock( + AbstractItem::class + ); + $childQuoteItemMock->method('getProduct')->willReturn($childProductMock); + + /** @var Product|MockObject $productMock */ + $productMock = $this->createPartialMock( + Product::class, + ['getTypeId'] + ); + $productMock->method('getTypeId')->willReturn(Configurable::TYPE_CODE); + /* @var AbstractItem|MockObject $quoteItemMock */ + $quoteItemMock = $this->createPartialMock( + AbstractItem::class, + ['getProduct', 'getHasChildren', 'getChildren', 'getQuote', 'getAddress', 'getOptionByCode'] + ); + $quoteItemMock->method('getProduct')->willReturn($productMock); + $quoteItemMock->method('getHasChildren')->willReturn(true); + $quoteItemMock->method('getChildren')->willReturn([$childQuoteItemMock]); + + /* @var TaxClassKeyInterface|MockObject $taxClassObjectMock */ + $taxClassObjectMock = $this->createMock(TaxClassKeyInterface::class); + $taxClassObjectMock->expects($this->once())->method('setValue')->with($childTaxClassId); + + /* @var QuoteDetailsItemInterface|MockObject $quoteDetailsItemMock */ + $quoteDetailsItemMock = $this->createMock(QuoteDetailsItemInterface::class); + $quoteDetailsItemMock->method('getTaxClassKey')->willReturn($taxClassObjectMock); + + $this->commonTaxCollectorPlugin->afterMapItem( + $this->createMock(CommonTaxCollector::class), + $quoteDetailsItemMock, + $this->createMock(QuoteDetailsItemInterfaceFactory::class), + $quoteItemMock + ); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php index 9fd225e8acaab..dcdffddbeeec0 100644 --- a/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php +++ b/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/ConfigurablePanel.php @@ -5,14 +5,14 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier; +use Magento\Catalog\Model\Locator\LocatorInterface; use Magento\Catalog\Model\Product\Attribute\Backend\Sku; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; +use Magento\Framework\UrlInterface; use Magento\Ui\Component\Container; -use Magento\Ui\Component\Form; use Magento\Ui\Component\DynamicRows; +use Magento\Ui\Component\Form; use Magento\Ui\Component\Modal; -use Magento\Framework\UrlInterface; -use Magento\Catalog\Model\Locator\LocatorInterface; /** * Data provider for Configurable panel @@ -90,7 +90,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -98,7 +98,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -197,7 +197,7 @@ public function modifyMeta(array $meta) 'autoRender' => false, 'componentType' => 'insertListing', 'component' => 'Magento_ConfigurableProduct/js' - .'/components/associated-product-insert-listing', + . '/components/associated-product-insert-listing', 'dataScope' => $this->associatedListingPrefix . static::ASSOCIATED_PRODUCT_LISTING, 'externalProvider' => $this->associatedListingPrefix @@ -328,14 +328,12 @@ protected function getButtonSet() 'component' => 'Magento_Ui/js/form/components/button', 'actions' => [ [ - 'targetName' => - $this->dataScopeName . '.configurableModal', + 'targetName' => $this->dataScopeName . '.configurableModal', 'actionName' => 'trigger', 'params' => ['active', true], ], [ - 'targetName' => - $this->dataScopeName . '.configurableModal', + 'targetName' => $this->dataScopeName . '.configurableModal', 'actionName' => 'openModal', ], ], @@ -574,6 +572,7 @@ protected function getColumn( 'dataType' => Form\Element\DataType\Text::NAME, 'dataScope' => $name, 'visibleIfCanEdit' => false, + 'labelVisible' => false, 'imports' => [ 'visible' => '!${$.provider}:${$.parentScope}.canEdit' ], @@ -592,6 +591,7 @@ protected function getColumn( 'component' => 'Magento_Ui/js/form/components/group', 'label' => $label, 'dataScope' => '', + 'showLabel' => false ]; $container['children'] = [ $name . '_edit' => $fieldEdit, diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index 74e611af2cbbc..c33dc148cddb1 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -22,7 +22,8 @@ "magento/module-sales-rule": "101.0.*", "magento/module-product-video": "100.2.*", "magento/module-configurable-sample-data": "Sample Data version:100.2.*", - "magento/module-product-links-sample-data": "Sample Data version:100.2.*" + "magento/module-product-links-sample-data": "Sample Data version:100.2.*", + "magento/module-tax": "100.2.*" }, "type": "magento2-module", "version": "100.2.7", diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index e6e0da721e150..a390cb375befc 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -245,4 +245,7 @@ </argument> </arguments> </type> + <type name="Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector"> + <plugin name="apply_tax_class_id" type="Magento\ConfigurableProduct\Plugin\Tax\Model\Sales\Total\Quote\CommonTaxCollector" /> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index a8712cdc183de..190ecccbfdb76 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -17,9 +17,9 @@ <legend class="legend admin__legend"> <span><?= /* @escapeNotVerified */ __('Associated Products') ?></span> </legend> - <div class="product-options"> - <div class="field admin__field _required required"> - <?php foreach ($_attributes as $_attribute): ?> + <div class="product-options fieldset admin__fieldset"> + <?php foreach ($_attributes as $_attribute): ?> + <div class="field admin__field _required required"> <label class="label admin__field-label"><?php /* @escapeNotVerified */ echo $_attribute->getProductAttribute() ->getStoreLabel($_product->getStoreId()); @@ -34,8 +34,8 @@ <option><?= /* @escapeNotVerified */ __('Choose an Option...') ?></option> </select> </div> - <?php endforeach; ?> - </div> + </div> + <?php endforeach; ?> </div> </fieldset> <script> diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php index 39a58ef360cb3..a9ae04cb0c5d1 100644 --- a/app/code/Magento/Cron/Model/Schedule.php +++ b/app/code/Magento/Cron/Model/Schedule.php @@ -9,6 +9,7 @@ use Magento\Framework\Exception\CronException; use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\Intl\DateTimeFactory; /** * Crontab schedule model @@ -50,13 +51,19 @@ class Schedule extends \Magento\Framework\Model\AbstractModel */ private $timezoneConverter; + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param TimezoneInterface $timezoneConverter + * @param TimezoneInterface|null $timezoneConverter + * @param DateTimeFactory|null $dateTimeFactory */ public function __construct( \Magento\Framework\Model\Context $context, @@ -64,10 +71,12 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - TimezoneInterface $timezoneConverter = null + TimezoneInterface $timezoneConverter = null, + DateTimeFactory $dateTimeFactory = null ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class); + $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class); } /** @@ -109,17 +118,20 @@ public function trySchedule() if (!$e || !$time) { return false; } + $configTimeZone = $this->timezoneConverter->getConfigTimezone(); + $storeDateTime = $this->dateTimeFactory->create(null, new \DateTimeZone($configTimeZone)); if (!is_numeric($time)) { //convert time from UTC to admin store timezone //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone - $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i'); - $time = strtotime($time); + $dateTimeUtc = $this->dateTimeFactory->create($time); + $time = $dateTimeUtc->getTimestamp(); } - $match = $this->matchCronExpression($e[0], strftime('%M', $time)) - && $this->matchCronExpression($e[1], strftime('%H', $time)) - && $this->matchCronExpression($e[2], strftime('%d', $time)) - && $this->matchCronExpression($e[3], strftime('%m', $time)) - && $this->matchCronExpression($e[4], strftime('%w', $time)); + $time = $storeDateTime->setTimestamp($time); + $match = $this->matchCronExpression($e[0], $time->format('i')) + && $this->matchCronExpression($e[1], $time->format('H')) + && $this->matchCronExpression($e[2], $time->format('d')) + && $this->matchCronExpression($e[3], $time->format('m')) + && $this->matchCronExpression($e[4], $time->format('w')); return $match; } diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php index e9f4c61c7f551..76e9627ad7098 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php @@ -6,6 +6,9 @@ namespace Magento\Cron\Test\Unit\Model; use Magento\Cron\Model\Schedule; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; /** * Class \Magento\Cron\Test\Unit\Model\ObserverTest @@ -18,11 +21,27 @@ class ScheduleTest extends \PHPUnit\Framework\TestCase */ protected $helper; + /** + * @var \Magento\Cron\Model\ResourceModel\Schedule + */ protected $resourceJobMock; + /** + * @var TimezoneInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $timezoneConverter; + + /** + * @var DateTimeFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $dateTimeFactory; + + /** + * @inheritdoc + */ protected function setUp() { - $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->helper = new ObjectManager($this); $this->resourceJobMock = $this->getMockBuilder(\Magento\Cron\Model\ResourceModel\Schedule::class) ->disableOriginalConstructor() @@ -32,18 +51,30 @@ protected function setUp() $this->resourceJobMock->expects($this->any()) ->method('getIdFieldName') ->will($this->returnValue('id')); + + $this->timezoneConverter = $this->getMockBuilder(TimezoneInterface::class) + ->setMethods(['date']) + ->getMockForAbstractClass(); + + $this->dateTimeFactory = $this->getMockBuilder(DateTimeFactory::class) + ->setMethods(['create']) + ->getMock(); } /** + * Test for SetCronExpr + * * @param string $cronExpression * @param array $expected + * + * @return void * @dataProvider setCronExprDataProvider */ public function testSetCronExpr($cronExpression, $expected) { // 1. Create mocks - /** @var \Magento\Cron\Model\Schedule $model */ - $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class); + /** @var Schedule $model */ + $model = $this->helper->getObject(Schedule::class); // 2. Run tested method $model->setCronExpr($cronExpression); @@ -61,7 +92,7 @@ public function testSetCronExpr($cronExpression, $expected) * * @return array */ - public function setCronExprDataProvider() + public function setCronExprDataProvider(): array { return [ ['1 2 3 4 5', [1, 2, 3, 4, 5]], @@ -121,27 +152,33 @@ public function setCronExprDataProvider() } /** + * Test for SetCronExprException + * * @param string $cronExpression + * + * @return void * @expectedException \Magento\Framework\Exception\CronException * @dataProvider setCronExprExceptionDataProvider */ public function testSetCronExprException($cronExpression) { // 1. Create mocks - /** @var \Magento\Cron\Model\Schedule $model */ - $model = $this->helper->getObject(\Magento\Cron\Model\Schedule::class); + /** @var Schedule $model */ + $model = $this->helper->getObject(Schedule::class); // 2. Run tested method $model->setCronExpr($cronExpression); } /** + * Data provider + * * Here is a list of allowed characters and values for Cron expression * http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm * * @return array */ - public function setCronExprExceptionDataProvider() + public function setCronExprExceptionDataProvider(): array { return [ [''], @@ -153,17 +190,31 @@ public function setCronExprExceptionDataProvider() } /** + * Test for trySchedule + * * @param int $scheduledAt * @param array $cronExprArr * @param $expected + * + * @return void * @dataProvider tryScheduleDataProvider */ public function testTrySchedule($scheduledAt, $cronExprArr, $expected) { // 1. Create mocks + $this->timezoneConverter->method('getConfigTimezone') + ->willReturn('UTC'); + + $this->dateTimeFactory->method('create') + ->willReturn(new \DateTime()); + /** @var \Magento\Cron\Model\Schedule $model */ $model = $this->helper->getObject( - \Magento\Cron\Model\Schedule::class + \Magento\Cron\Model\Schedule::class, + [ + 'timezoneConverter' => $this->timezoneConverter, + 'dateTimeFactory' => $this->dateTimeFactory, + ] ); // 2. Set fixtures @@ -177,22 +228,29 @@ public function testTrySchedule($scheduledAt, $cronExprArr, $expected) $this->assertEquals($expected, $result); } + /** + * Test for tryScheduleWithConversionToAdminStoreTime + * + * @return void + */ public function testTryScheduleWithConversionToAdminStoreTime() { $scheduledAt = '2011-12-13 14:15:16'; $cronExprArr = ['*', '*', '*', '*', '*']; - // 1. Create mocks - $timezoneConverter = $this->createMock(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); - $timezoneConverter->expects($this->once()) - ->method('date') - ->with($scheduledAt) - ->willReturn(new \DateTime($scheduledAt)); + $this->timezoneConverter->method('getConfigTimezone') + ->willReturn('UTC'); + + $this->dateTimeFactory->method('create') + ->willReturn(new \DateTime()); /** @var \Magento\Cron\Model\Schedule $model */ $model = $this->helper->getObject( \Magento\Cron\Model\Schedule::class, - ['timezoneConverter' => $timezoneConverter] + [ + 'timezoneConverter' => $this->timezoneConverter, + 'dateTimeFactory' => $this->dateTimeFactory, + ] ); // 2. Set fixtures @@ -207,11 +265,15 @@ public function testTryScheduleWithConversionToAdminStoreTime() } /** + * Data provider + * * @return array */ - public function tryScheduleDataProvider() + public function tryScheduleDataProvider(): array { $date = '2011-12-13 14:15:16'; + $timestamp = (new \DateTime($date))->getTimestamp(); + $day = 'Monday'; return [ [$date, [], false], [$date, null, false], @@ -219,19 +281,23 @@ public function tryScheduleDataProvider() [$date, [], false], [$date, null, false], [$date, false, false], - [strtotime($date), ['*', '*', '*', '*', '*'], true], - [strtotime($date), ['15', '*', '*', '*', '*'], true], - [strtotime($date), ['*', '14', '*', '*', '*'], true], - [strtotime($date), ['*', '*', '13', '*', '*'], true], - [strtotime($date), ['*', '*', '*', '12', '*'], true], - [strtotime('Monday'), ['*', '*', '*', '*', '1'], true], + [$timestamp, ['*', '*', '*', '*', '*'], true], + [$timestamp, ['15', '*', '*', '*', '*'], true], + [$timestamp, ['*', '14', '*', '*', '*'], true], + [$timestamp, ['*', '*', '13', '*', '*'], true], + [$timestamp, ['*', '*', '*', '12', '*'], true], + [(new \DateTime($day))->getTimestamp(), ['*', '*', '*', '*', '1'], true], ]; } /** + * Test for matchCronExpression + * * @param string $cronExpressionPart * @param int $dateTimePart * @param bool $expectedResult + * + * @return void * @dataProvider matchCronExpressionDataProvider */ public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $expectedResult) @@ -248,9 +314,11 @@ public function testMatchCronExpression($cronExpressionPart, $dateTimePart, $exp } /** + * Data provider + * * @return array */ - public function matchCronExpressionDataProvider() + public function matchCronExpressionDataProvider(): array { return [ ['*', 0, true], @@ -287,7 +355,11 @@ public function matchCronExpressionDataProvider() } /** + * Test for matchCronExpressionException + * * @param string $cronExpressionPart + * + * @return void * @expectedException \Magento\Framework\Exception\CronException * @dataProvider matchCronExpressionExceptionDataProvider */ @@ -304,9 +376,11 @@ public function testMatchCronExpressionException($cronExpressionPart) } /** + * Data provider + * * @return array */ - public function matchCronExpressionExceptionDataProvider() + public function matchCronExpressionExceptionDataProvider(): array { return [ ['1/2/3'], //Invalid cron expression, expecting 'match/modulus': 1/2/3 @@ -317,8 +391,12 @@ public function matchCronExpressionExceptionDataProvider() } /** + * Test for GetNumeric + * * @param mixed $param * @param int $expectedResult + * + * @return void * @dataProvider getNumericDataProvider */ public function testGetNumeric($param, $expectedResult) @@ -335,9 +413,11 @@ public function testGetNumeric($param, $expectedResult) } /** + * Data provider + * * @return array */ - public function getNumericDataProvider() + public function getNumericDataProvider(): array { return [ [null, false], @@ -362,6 +442,11 @@ public function getNumericDataProvider() ]; } + /** + * Test for tryLockJobSuccess + * + * @return void + */ public function testTryLockJobSuccess() { $scheduleId = 1; @@ -386,6 +471,11 @@ public function testTryLockJobSuccess() $this->assertEquals(Schedule::STATUS_RUNNING, $model->getStatus()); } + /** + * Test for tryLockJobFailure + * + * @return void + */ public function testTryLockJobFailure() { $scheduleId = 1; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php index 9a025211c9b0a..0aeed1562c51e 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php @@ -48,7 +48,7 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele $regionId = $element->getForm()->getElement('region_id')->getValue(); - $html = '<div class="field field-state required admin__field _required">'; + $html = '<div class="field field-state admin__field">'; $element->setClass('input-text admin__control-text'); $element->setRequired(true); $html .= $element->getLabelHtml() . '<div class="control admin__field-control">'; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php index bb190260e4776..c1266febff99d 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Orders.php @@ -63,7 +63,8 @@ protected function _construct() { parent::_construct(); $this->setId('customer_orders_grid'); - $this->setDefaultSort('created_at', 'desc'); + $this->setDefaultSort('created_at'); + $this->setDefaultDir('desc'); $this->setUseAjax(true); } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php index 3f2c7cda7608d..988a157805b36 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/View/Cart.php @@ -77,7 +77,8 @@ protected function _construct() { parent::_construct(); $this->setId('customer_view_cart_grid'); - $this->setDefaultSort('added_at', 'desc'); + $this->setDefaultSort('added_at'); + $this->setDefaultDir('desc'); $this->setSortable(false); $this->setPagerVisibility(false); $this->setFilterVisibility(false); @@ -94,7 +95,7 @@ protected function _prepareCollection() $quote = $this->getQuote(); if ($quote) { - $collection = $quote->getItemsCollection(false); + $collection = $quote->getItemsCollection(true); } else { $collection = $this->_dataCollectionFactory->create(); } diff --git a/app/code/Magento/Customer/Block/Form/Login.php b/app/code/Magento/Customer/Block/Form/Login.php index 7b265ae1f0f32..d3d3306a49b44 100644 --- a/app/code/Magento/Customer/Block/Form/Login.php +++ b/app/code/Magento/Customer/Block/Form/Login.php @@ -47,15 +47,6 @@ public function __construct( $this->_customerSession = $customerSession; } - /** - * @return $this - */ - protected function _prepareLayout() - { - $this->pageConfig->getTitle()->set(__('Customer Login')); - return parent::_prepareLayout(); - } - /** * Retrieve form posting url * diff --git a/app/code/Magento/Customer/Block/Widget/Name.php b/app/code/Magento/Customer/Block/Widget/Name.php index 35f3bbefb8f00..2576545601c73 100644 --- a/app/code/Magento/Customer/Block/Widget/Name.php +++ b/app/code/Magento/Customer/Block/Widget/Name.php @@ -245,10 +245,14 @@ public function getStoreLabel($attributeCode) */ public function getAttributeValidationClass($attributeCode) { - return $this->_addressHelper->getAttributeValidationClass($attributeCode); + $attributeMetadata = $this->_getAttribute($attributeCode); + + return $attributeMetadata ? $attributeMetadata->getFrontendClass() : ''; } /** + * Check if attribute is required + * * @param string $attributeCode * @return bool */ @@ -259,6 +263,8 @@ private function _isAttributeRequired($attributeCode) } /** + * Check if attribute is visible + * * @param string $attributeCode * @return bool */ diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index 3a03e9064a0a3..561039990f705 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -283,11 +283,9 @@ protected function _extractCustomerAddressData(array & $extractedCustomerData) public function execute() { $returnToEdit = false; - $originalRequestData = $this->getRequest()->getPostValue(); - $customerId = $this->getCurrentCustomerId(); - if ($originalRequestData) { + if ($this->getRequest()->getPostValue()) { try { // optional fields might be set in request for future processing by observers in other modules $customerData = $this->_extractCustomerData(); @@ -375,7 +373,7 @@ public function execute() $messages = $exception->getMessage(); } $this->_addSessionErrorMessages($messages); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (\Magento\Framework\Exception\AbstractAggregateException $exception) { $errors = $exception->getErrors(); @@ -384,18 +382,19 @@ public function execute() $messages[] = $error->getMessage(); } $this->_addSessionErrorMessages($messages); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (LocalizedException $exception) { $this->_addSessionErrorMessages($exception->getMessage()); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } catch (\Exception $exception) { $this->messageManager->addException($exception, __('Something went wrong while saving the customer.')); - $this->_getSession()->setCustomerFormData($originalRequestData); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); $returnToEdit = true; } } + $resultRedirect = $this->resultRedirectFactory->create(); if ($returnToEdit) { if ($customerId) { @@ -501,4 +500,29 @@ private function disableAddressValidation(CustomerInterface $customer) $addressModel->setShouldIgnoreValidation(true); } } + + /** + * Retrieve formatted form data + * + * @return array + */ + private function retrieveFormattedFormData(): array + { + $originalRequestData = $this->getRequest()->getPostValue(); + + /* Customer data filtration */ + if (isset($originalRequestData['customer'])) { + $customerData = $this->_extractData( + 'adminhtml_customer', + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + [], + 'customer' + ); + + $customerData = array_intersect_key($customerData, $originalRequestData['customer']); + $originalRequestData['customer'] = array_merge($originalRequestData['customer'], $customerData); + } + + return $originalRequestData; + } } diff --git a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php index aa73e275ee0ca..f82a4d15ae8bf 100644 --- a/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php +++ b/app/code/Magento/Customer/CustomerData/Plugin/SessionChecker.php @@ -5,10 +5,13 @@ */ namespace Magento\Customer\CustomerData\Plugin; -use Magento\Framework\Session\SessionManager; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; use Magento\Framework\Stdlib\Cookie\PhpCookieManager; +/** + * Class SessionChecker + */ class SessionChecker { /** @@ -36,10 +39,12 @@ public function __construct( /** * Delete frontend session cookie if customer session is expired * - * @param SessionManager $sessionManager + * @param SessionManagerInterface $sessionManager * @return void + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException */ - public function beforeStart(SessionManager $sessionManager) + public function beforeStart(SessionManagerInterface $sessionManager) { if (!$this->cookieManager->getCookie($sessionManager->getName()) && $this->cookieManager->getCookie('mage-cache-sessid') diff --git a/app/code/Magento/Customer/Model/Options.php b/app/code/Magento/Customer/Model/Options.php index 7747e309d82a6..f8761b4888a32 100644 --- a/app/code/Magento/Customer/Model/Options.php +++ b/app/code/Magento/Customer/Model/Options.php @@ -8,7 +8,11 @@ use Magento\Config\Model\Config\Source\Nooptreq as NooptreqSource; use Magento\Customer\Helper\Address as AddressHelper; use Magento\Framework\Escaper; +use Magento\Store\Api\Data\StoreInterface; +/** + * Customer Options. + */ class Options { /** @@ -38,7 +42,7 @@ public function __construct( /** * Retrieve name prefix dropdown options * - * @param null $store + * @param null|string|bool|int|StoreInterface $store * @return array|bool */ public function getNamePrefixOptions($store = null) @@ -52,7 +56,7 @@ public function getNamePrefixOptions($store = null) /** * Retrieve name suffix dropdown options * - * @param null $store + * @param null|string|bool|int|StoreInterface $store * @return array|bool */ public function getNameSuffixOptions($store = null) @@ -64,7 +68,9 @@ public function getNameSuffixOptions($store = null) } /** - * @param $options + * Unserialize and clear name prefix or suffix options. + * + * @param string $options * @param bool $isOptional * @return array|bool * @@ -91,7 +97,7 @@ private function prepareNamePrefixSuffixOptions($options, $isOptional = false) return false; } $result = []; - $options = explode(';', $options); + $options = array_filter(explode(';', $options)); foreach ($options as $value) { $value = $this->escaper->escapeHtml(trim($value)); $result[$value] = $value; diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml new file mode 100644 index 0000000000000..1ffc258e78a43 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAddCustomerAddressActionGroup.xml @@ -0,0 +1,25 @@ +<?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="AdminAddCustomerAddressWithRegionTypeSelectActionGroup" > + <arguments> + <argument name="customerAddress" defaultValue="CustomerAddressSimple"/> + </arguments> + <click selector="{{AdminCustomerAccountAddressSection.addresses}}" stepKey="proceedToAddresses"/> + <click selector="{{AdminCustomerAccountAddressSection.addNewAddress}}" stepKey="addNewAddresses"/> + <fillField userInput="{{customerAddress.street[0]}}" selector="{{AdminCustomerAccountNewAddressSection.street}}" stepKey="fillStreetAddress"/> + <fillField userInput="{{customerAddress.city}}" selector="{{AdminCustomerAccountNewAddressSection.city}}" stepKey="fillCity"/> + <selectOption userInput="{{customerAddress.country_id}}" selector="{{AdminCustomerAccountNewAddressSection.country}}" stepKey="selectCountry"/> + <selectOption userInput="{{customerAddress.state}}" selector="{{AdminCustomerAccountNewAddressSection.regionId}}" stepKey="selectState"/> + <fillField userInput="{{customerAddress.postcode}}" selector="{{AdminCustomerAccountNewAddressSection.zip}}" stepKey="fillZipCode"/> + <fillField userInput="{{customerAddress.telephone}}" selector="{{AdminCustomerAccountNewAddressSection.phone}}" stepKey="fillPhone"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveCustomer"/> + <see userInput="You saved the customer." selector="{{AdminMessagesSection.success}}" stepKey="customerIsSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml index a8b8cece39fad..c3efa0fbc880c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml @@ -21,6 +21,7 @@ <data key="firstname">John</data> <data key="lastname">Doe</data> <data key="middlename">S</data> + <data key="fullname">John Doe</data> <data key="password">pwdTest123!</data> <data key="prefix">Mr</data> <data key="suffix">Sr</data> @@ -44,6 +45,16 @@ <data key="website_id">0</data> <requiredEntity type="address">US_Address_TX</requiredEntity> </entity> + <entity name="Simple_Customer_Without_Address" type="customer"> + <data key="group_id">1</data> + <data key="email" unique="prefix">John.Doe@example.com</data> + <data key="firstname">John</data> + <data key="lastname">Doe</data> + <data key="fullname">John Doe</data> + <data key="password">pwdTest123!</data> + <data key="store_id">0</data> + <data key="website_id">0</data> + </entity> <entity name="Simple_US_Customer_For_Update" type="customer"> <var key="id" entityKey="id" entityType="customer"/> <data key="firstname">Jane</data> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml new file mode 100644 index 0000000000000..1331f288286e5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerNameAddressOptionsConfigData.xml @@ -0,0 +1,38 @@ +<?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="CustomerPrefixOptions" type="customer_name_address_options"> + <requiredEntity type="prefix_options">PrefixOptions</requiredEntity> + </entity> + <entity name="PrefixOptions" type="prefix_options"> + <data key="value">Mr;Mrs;Ms;Dr</data> + </entity> + + <entity name="DefaultCustomerPrefixOptions" type="customer_name_address_options"> + <requiredEntity type="prefix_options">DefaultPrefixOptions</requiredEntity> + </entity> + <entity name="DefaultPrefixOptions" type="prefix_options"> + <data key="value"></data> + </entity> + + <entity name="CustomerSuffixOptions" type="customer_name_address_options"> + <requiredEntity type="suffix_options">SuffixOptions</requiredEntity> + </entity> + <entity name="SuffixOptions" type="suffix_options"> + <data key="value">Jr;Sr</data> + </entity> + + <entity name="DefaultCustomerSuffixOptions" type="customer_name_address_options"> + <requiredEntity type="suffix_options">DefaultSuffixOptions</requiredEntity> + </entity> + <entity name="DefaultSuffixOptions" type="suffix_options"> + <data key="value"></data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml new file mode 100644 index 0000000000000..07175a09fe3e1 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/customer_name_address_options_config-meta.xml @@ -0,0 +1,25 @@ +<?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="CustomerConfigNameAddressOptions" dataType="customer_name_address_options" type="create" auth="adminFormKey" url="/admin/system_config/save/section/customer/" method="POST" successRegex="/messages-message-success/"> + <object key="groups" dataType="customer_name_address_options"> + <object key="address" dataType="customer_name_address_options"> + <object key="fields" dataType="customer_name_address_options"> + <object key="prefix_options" dataType="prefix_options"> + <field key="value">string</field> + </object> + <object key="suffix_options" dataType="suffix_options"> + <field key="value">string</field> + </object> + </object> + </object> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml index 72d5d90bdc05f..7cd36c12c80bd 100644 --- a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml @@ -12,5 +12,6 @@ <section name="AdminCustomerMainActionsSection"/> <section name="AdminCustomerAccountAddressSection"/> <section name="AdminCustomerAccountEditAddressSection"/> + <section name="AdminCustomerAccountNewAddressSection"/> </page> </pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml index db9619dde671f..70042e2a71467 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountAddressSection.xml @@ -7,7 +7,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerAccountAddressSection"> - <element name="addresses" type="button" selector="a#tab_address"/> - <element name="addNewAddress" type="button" selector=".address-list-actions button.scalable.add span"/> + <element name="addresses" type="button" selector="#tab_address" timeout="30"/> + <element name="addNewAddress" type="button" selector=".address-list-actions button.scalable.add span" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml new file mode 100644 index 0000000000000..8445343c9b9c0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountNewAddressSection.xml @@ -0,0 +1,19 @@ +<!-- + /** + * 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="AdminCustomerAccountNewAddressSection"> + <element name="firstName" type="button" selector="input[name*='address'][name*='new'][name*='firstname']"/> + <element name="lastName" type="button" selector="input[name*='address'][name*='new'][name*='lastname']"/> + <element name="street" type="button" selector="input[name*='new'][name*='street']"/> + <element name="city" type="input" selector="input[name*='new'][name*='city']"/> + <element name="country" type="select" selector="select[name*='address'][name*='new'][name*='country']" timeout="10"/> + <element name="regionId" type="select" selector="select[name*='address'][name*='new'][name*='region_id']"/> + <element name="zip" type="input" selector="input[name*='address'][name*='new'][name*='postcode']"/> + <element name="phone" type="text" selector="input[name*='address'][name*='new'][name*='telephone']" /> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml index 0a77890033295..9553752539757 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml @@ -7,8 +7,9 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCustomerMainActionsSection"> <element name="saveButton" type="button" selector="#save" timeout="30"/> + <element name="deleteButton" type="button" selector="div.page-actions button.delete" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index e703e7499d731..09082a0a9de53 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php @@ -886,22 +886,24 @@ public function testExecuteWithNewCustomerAndValidationException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -912,7 +914,7 @@ public function testExecuteWithNewCustomerAndValidationException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPost') ->willReturnMap( [ @@ -925,12 +927,12 @@ public function testExecuteWithNewCustomerAndValidationException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -938,19 +940,19 @@ public function testExecuteWithNewCustomerAndValidationException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -998,7 +1000,10 @@ public function testExecuteWithNewCustomerAndValidationException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) @@ -1029,22 +1034,24 @@ public function testExecuteWithNewCustomerAndLocalizedException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -1055,7 +1062,7 @@ public function testExecuteWithNewCustomerAndLocalizedException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPost') ->willReturnMap( [ @@ -1068,12 +1075,12 @@ public function testExecuteWithNewCustomerAndLocalizedException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -1082,19 +1089,19 @@ public function testExecuteWithNewCustomerAndLocalizedException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -1141,7 +1148,10 @@ public function testExecuteWithNewCustomerAndLocalizedException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) @@ -1172,22 +1182,24 @@ public function testExecuteWithNewCustomerAndException() 'customer' => [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '3/12/1996', ], 'subscription' => $subscription, ]; $extractedData = [ 'coolness' => false, 'disable_auto_group_change' => 'false', + 'dob' => '1996-03-12', ]; /** @var AttributeMetadataInterface|\PHPUnit_Framework_MockObject_MockObject $customerFormMock */ $attributeMock = $this->getMockBuilder( \Magento\Customer\Api\Data\AttributeMetadataInterface::class )->disableOriginalConstructor()->getMock(); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->once()) + $attributeMock->expects($this->exactly(2)) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -1198,7 +1210,7 @@ public function testExecuteWithNewCustomerAndException() [null, null, $postValue], [CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, null, $postValue['customer']], ]); - $this->requestMock->expects($this->exactly(2)) + $this->requestMock->expects($this->any()) ->method('getPost') ->willReturnMap( [ @@ -1211,12 +1223,12 @@ public function testExecuteWithNewCustomerAndException() $objectMock = $this->getMockBuilder(\Magento\Framework\DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->once()) + $objectMock->expects($this->exactly(2)) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->once()) + $this->objectFactoryMock->expects($this->exactly(2)) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -1224,19 +1236,19 @@ public function testExecuteWithNewCustomerAndException() $customerFormMock = $this->getMockBuilder( \Magento\Customer\Model\Metadata\Form::class )->disableOriginalConstructor()->getMock(); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->once()) + $customerFormMock->expects($this->exactly(2)) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->once()) + $this->formFactoryMock->expects($this->exactly(2)) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -1285,7 +1297,10 @@ public function testExecuteWithNewCustomerAndException() $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') - ->with($postValue); + ->with([ + 'customer' => $extractedData, + 'subscription' => $subscription, + ]); /** @var Redirect|\PHPUnit_Framework_MockObject_MockObject $redirectMock */ $redirectMock = $this->getMockBuilder(\Magento\Framework\Controller\Result\Redirect::class) diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 4a45c4ad48d19..c31742519e581 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -57,7 +57,7 @@ <type name="Magento\Checkout\Block\Cart\Sidebar"> <plugin name="customer_cart" type="Magento\Customer\Model\Cart\ConfigPlugin" /> </type> - <type name="Magento\Framework\Session\SessionManager"> + <type name="Magento\Framework\Session\SessionManagerInterface"> <plugin name="session_checker" type="Magento\Customer\CustomerData\Plugin\SessionChecker" /> </type> <type name="Magento\Authorization\Model\CompositeUserContext"> @@ -77,4 +77,4 @@ </argument> </arguments> </type> -</config> +</config> \ No newline at end of file diff --git a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml index d49dae6dee58f..3518df736c4ac 100644 --- a/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml +++ b/app/code/Magento/Customer/view/frontend/layout/customer_account_login.xml @@ -6,6 +6,9 @@ */ --> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <title>Customer Login + diff --git a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php index da5ad64f8b239..02fed61c44863 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Country/Collection.php @@ -328,7 +328,7 @@ private function addDefaultCountryToOptions(array &$options) foreach ($options as $key => $option) { if (isset($defaultCountry[$option['value']])) { - $options[$key]['is_default'] = $defaultCountry[$option['value']]; + $options[$key]['is_default'] = !empty($defaultCountry[$option['value']]); } } } diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index ffbcce11cb4f6..5339b0c9eb5bd 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -216,7 +216,7 @@ protected function _getRatesByCode($code, $toCurrencies = null) $connection = $this->getConnection(); $bind = [':currency_from' => $code]; $select = $connection->select()->from( - $this->getTable('directory_currency_rate'), + $this->_currencyRateTable, ['currency_to', 'rate'] )->where( 'currency_from = :currency_from' diff --git a/app/code/Magento/Downloadable/Helper/Download.php b/app/code/Magento/Downloadable/Helper/Download.php index 910eaa42f0e84..e98d400794dd1 100644 --- a/app/code/Magento/Downloadable/Helper/Download.php +++ b/app/code/Magento/Downloadable/Helper/Download.php @@ -15,6 +15,7 @@ /** * Downloadable Products Download Helper * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Download extends \Magento\Framework\App\Helper\AbstractHelper { @@ -188,19 +189,20 @@ public function getFileSize() public function getContentType() { $this->_getHandle(); - if ($this->_linkType == self::LINK_TYPE_FILE) { - if (function_exists( - 'mime_content_type' - ) && ($contentType = mime_content_type( - $this->_workingDirectory->getAbsolutePath($this->_resourceFile) - )) + if ($this->_linkType === self::LINK_TYPE_FILE) { + if (function_exists('mime_content_type') + && ($contentType = mime_content_type( + $this->_workingDirectory->getAbsolutePath($this->_resourceFile) + )) ) { return $contentType; - } else { - return $this->_downloadableFile->getFileType($this->_resourceFile); } - } elseif ($this->_linkType == self::LINK_TYPE_URL) { - return $this->_handle->stat($this->_resourceFile)['type']; + return $this->_downloadableFile->getFileType($this->_resourceFile); + } + if ($this->_linkType === self::LINK_TYPE_URL) { + return (is_array($this->_handle->stat($this->_resourceFile)['type']) + ? end($this->_handle->stat($this->_resourceFile)['type']) + : $this->_handle->stat($this->_resourceFile)['type']); } return $this->_contentType; } @@ -254,10 +256,21 @@ public function setResource($resourceFile, $linkType = self::LINK_TYPE_FILE) ); } } - + $this->_resourceFile = $resourceFile; + + /** + * check header for urls + */ + if ($linkType === self::LINK_TYPE_URL) { + $headers = array_change_key_case(get_headers($this->_resourceFile, 1), CASE_LOWER); + if (isset($headers['location'])) { + $this->_resourceFile = is_array($headers['location']) ? current($headers['location']) + : $headers['location']; + } + } + $this->_linkType = $linkType; - return $this; } diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php index a352c4bdf7bc3..9ab664cb27839 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Links.php @@ -3,22 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Downloadable\Model\Product\Type; -use Magento\Downloadable\Model\Source\TypeUpload; use Magento\Downloadable\Model\Source\Shareable; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Downloadable\Model\Source\TypeUpload; use Magento\Framework\Stdlib\ArrayManager; -use Magento\Ui\Component\DynamicRows; use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Container; +use Magento\Ui\Component\DynamicRows; use Magento\Ui\Component\Form; /** - * Class adds a grid with links + * Class adds a grid with links. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Links extends AbstractModifier @@ -86,7 +88,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -101,7 +103,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -160,6 +162,8 @@ public function modifyMeta(array $meta) } /** + * Get dynamic rows meta. + * * @return array */ protected function getDynamicRows() @@ -180,6 +184,8 @@ protected function getDynamicRows() } /** + * Get single link record meta. + * * @return array */ protected function getRecord() @@ -221,6 +227,8 @@ protected function getRecord() } /** + * Get link title meta. + * * @return array */ protected function getTitleColumn() @@ -232,6 +240,7 @@ protected function getTitleColumn() 'label' => __('Title'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 10, ]; $titleField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, @@ -247,6 +256,8 @@ protected function getTitleColumn() } /** + * Get link price meta. + * * @return array */ protected function getPriceColumn() @@ -258,6 +269,7 @@ protected function getPriceColumn() 'label' => __('Price'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 20, ]; $priceField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, @@ -281,6 +293,8 @@ protected function getPriceColumn() } /** + * Get link file element meta. + * * @return array */ protected function getFileColumn() @@ -292,6 +306,7 @@ protected function getFileColumn() 'label' => __('File'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 30, ]; $fileTypeField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, @@ -344,6 +359,8 @@ protected function getFileColumn() } /** + * Get sample container meta. + * * @return array */ protected function getSampleColumn() @@ -355,6 +372,7 @@ protected function getSampleColumn() 'label' => __('Sample'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 40, ]; $sampleTypeField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, @@ -403,6 +421,8 @@ protected function getSampleColumn() } /** + * Get link "is sharable" element meta. + * * @return array */ protected function getShareableColumn() @@ -413,6 +433,7 @@ protected function getShareableColumn() 'componentType' => Form\Field::NAME, 'dataType' => Form\Element\DataType\Number::NAME, 'dataScope' => 'is_shareable', + 'sortOrder' => 50, 'options' => $this->shareable->toOptionArray(), ]; @@ -420,6 +441,8 @@ protected function getShareableColumn() } /** + * Get link "max downloads" element meta. + * * @return array */ protected function getMaxDownloadsColumn() @@ -431,6 +454,7 @@ protected function getMaxDownloadsColumn() 'label' => __('Max. Downloads'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 60, ]; $numberOfDownloadsField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php index 1587163ba8121..3890ee5b9e2b2 100644 --- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php +++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Samples.php @@ -3,21 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier; use Magento\Downloadable\Model\Product\Type; use Magento\Downloadable\Model\Source\TypeUpload; -use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Stdlib\ArrayManager; -use Magento\Ui\Component\DynamicRows; use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Container; +use Magento\Ui\Component\DynamicRows; use Magento\Ui\Component\Form; /** - * Class adds a grid with samples + * Class adds a grid with samples. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Samples extends AbstractModifier @@ -77,7 +79,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function modifyData(array $data) { @@ -90,7 +92,7 @@ public function modifyData(array $data) } /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function modifyMeta(array $meta) @@ -135,6 +137,8 @@ public function modifyMeta(array $meta) } /** + * Get sample rows meta. + * * @return array */ protected function getDynamicRows() @@ -155,6 +159,8 @@ protected function getDynamicRows() } /** + * Get single sample row meta. + * * @return array */ protected function getRecord() @@ -192,6 +198,8 @@ protected function getRecord() } /** + * Get sample title meta. + * * @return array */ protected function getTitleColumn() @@ -203,6 +211,7 @@ protected function getTitleColumn() 'showLabel' => false, 'label' => __('Title'), 'dataScope' => '', + 'sortOrder' => 10, ]; $titleField['arguments']['data']['config'] = [ 'formElement' => Form\Element\Input::NAME, @@ -218,6 +227,8 @@ protected function getTitleColumn() } /** + * Get sample element meta. + * * @return array */ protected function getSampleColumn() @@ -229,6 +240,7 @@ protected function getSampleColumn() 'label' => __('File'), 'showLabel' => false, 'dataScope' => '', + 'sortOrder' => 20, ]; $sampleType['arguments']['data']['config'] = [ 'formElement' => Form\Element\Select::NAME, diff --git a/app/code/Magento/Eav/Model/Attribute/Data/File.php b/app/code/Magento/Eav/Model/Attribute/Data/File.php index d6476eefc59b1..1ee66915453c3 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/File.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/File.php @@ -146,7 +146,7 @@ protected function _validateByRules($value) return $this->_fileValidator->getMessages(); } - if (!empty($value['tmp_name']) && !is_uploaded_file($value['tmp_name'])) { + if (!empty($value['tmp_name']) && !file_exists($value['tmp_name'])) { return [__('"%1" is not a valid file.', $label)]; } diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index 40b884aec529f..7f6734b8b6b60 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -12,6 +12,7 @@ use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend; use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource; +use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface; use Magento\Framework\App\Config\Element; use Magento\Framework\DataObject; use Magento\Framework\DB\Adapter\DuplicateException; @@ -217,12 +218,21 @@ abstract class AbstractEntity extends AbstractResource implements EntityInterfac */ protected $objectRelationProcessor; + /** + * @var UniqueValidationInterface + */ + private $uniqueValidator; + /** * @param Context $context * @param array $data + * @param UniqueValidationInterface|null $uniqueValidator */ - public function __construct(Context $context, $data = []) - { + public function __construct( + Context $context, + $data = [], + UniqueValidationInterface $uniqueValidator = null + ) { $this->_eavConfig = $context->getEavConfig(); $this->_resource = $context->getResource(); $this->_attrSetEntity = $context->getAttributeSetEntity(); @@ -231,6 +241,8 @@ public function __construct(Context $context, $data = []) $this->_universalFactory = $context->getUniversalFactory(); $this->transactionManager = $context->getTransactionManager(); $this->objectRelationProcessor = $context->getObjectRelationProcessor(); + $this->uniqueValidator = $uniqueValidator ?: + ObjectManager::getInstance()->get(UniqueValidationInterface::class); parent::__construct(); $properties = get_object_vars($this); foreach ($data as $key => $value) { @@ -499,6 +511,7 @@ public function addAttributeByScope(AbstractAttribute $attribute, $entity = null /** * Get attributes by scope * + * @param string $suffix * @return array */ private function getAttributesByScope($suffix) @@ -969,12 +982,8 @@ public function checkAttributeUniqueValue(AbstractAttribute $attribute, $object) $data = $connection->fetchCol($select, $bind); - $objectId = $object->getData($entityIdField); - if ($objectId) { - if (isset($data[0])) { - return $data[0] == $objectId; - } - return true; + if ($object->getData($entityIdField)) { + return $this->uniqueValidator->validate($attribute, $object, $this, $entityIdField, $data); } return !count($data); @@ -1686,14 +1695,16 @@ public function saveAttribute(DataObject $object, $attributeCode) $connection->beginTransaction(); try { - $select = $connection->select()->from($table, 'value_id')->where($where); - $origValueId = $connection->fetchOne($select); + $select = $connection->select()->from($table, ['value_id', 'value'])->where($where); + $origRow = $connection->fetchRow($select); + $origValueId = $origRow['value_id'] ?? false; + $origValue = $origRow['value'] ?? null; if ($origValueId === false && $newValue !== null) { $this->_insertAttribute($object, $attribute, $newValue); } elseif ($origValueId !== false && $newValue !== null) { $this->_updateAttribute($object, $attribute, $origValueId, $newValue); - } elseif ($origValueId !== false && $newValue === null) { + } elseif ($origValueId !== false && $newValue === null && $origValue !== null) { $connection->delete($table, $where); } $this->_processAttributeValues(); @@ -1984,7 +1995,8 @@ public function afterDelete(DataObject $object) /** * Load attributes for object - * if the object will not pass all attributes for this entity type will be loaded + * + * If the object will not pass all attributes for this entity type will be loaded * * @param array $attributes * @param AbstractEntity|null $object diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php index 0991b3f9f4b23..36ad026029056 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/AbstractSource.php @@ -73,13 +73,15 @@ public function getOptionText($value) } } // End - if (isset($options[$value])) { + if (is_scalar($value) && isset($options[$value])) { return $options[$value]; } return false; } /** + * Get option id. + * * @param string $value * @return null|string */ diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php new file mode 100644 index 0000000000000..50a6ff9329fc9 --- /dev/null +++ b/app/code/Magento/Eav/Model/Entity/Attribute/UniqueValidationInterface.php @@ -0,0 +1,35 @@ +getData($entityLinkField); + } + + return true; + } +} diff --git a/app/code/Magento/Eav/Model/Entity/Type.php b/app/code/Magento/Eav/Model/Entity/Type.php index aa298d7d547bf..5ee15287e8899 100644 --- a/app/code/Magento/Eav/Model/Entity/Type.php +++ b/app/code/Magento/Eav/Model/Entity/Type.php @@ -167,11 +167,8 @@ public function getAttributeCollection($setId = null) */ protected function _getAttributeCollection() { - $collection = $this->_attributeFactory->create()->getCollection(); - $objectsModel = $this->getAttributeModel(); - if ($objectsModel) { - $collection->setModel($objectsModel); - } + $collection = $this->_universalFactory->create($this->getEntityAttributeCollection()); + $collection->setItemObjectClass($this->getAttributeModel()); return $collection; } diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index cd2fe7477ca60..18bb62b3f7d93 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -5,13 +5,19 @@ */ namespace Magento\Eav\Model\ResourceModel; +use Magento\Eav\Model\Config; use Magento\Framework\DataObject; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\AttributeInterface; use Magento\Framework\Model\Entity\ScopeInterface; use Magento\Framework\Model\Entity\ScopeResolver; use Psr\Log\LoggerInterface; +/** + * EAV read handler. + */ class ReadHandler implements AttributeInterface { /** @@ -30,23 +36,21 @@ class ReadHandler implements AttributeInterface private $logger; /** - * @var \Magento\Eav\Model\Config + * @var Config */ private $config; /** - * ReadHandler constructor. - * * @param MetadataPool $metadataPool * @param ScopeResolver $scopeResolver * @param LoggerInterface $logger - * @param \Magento\Eav\Model\Config $config + * @param Config $config */ public function __construct( MetadataPool $metadataPool, ScopeResolver $scopeResolver, LoggerInterface $logger, - \Magento\Eav\Model\Config $config + Config $config ) { $this->metadataPool = $metadataPool; $this->scopeResolver = $scopeResolver; @@ -86,6 +90,8 @@ private function getEntityAttributes(string $entityType, DataObject $entity): ar } /** + * Get context variables. + * * @param ScopeInterface $scope * @return array */ @@ -99,6 +105,8 @@ protected function getContextVariables(ScopeInterface $scope) } /** + * Execute read handler. + * * @param string $entityType * @param array $entityData * @param array $arguments @@ -129,33 +137,40 @@ public function execute($entityType, $entityData, $arguments = []) } } if (count($attributeTables)) { - $attributeTables = array_keys($attributeTables); - foreach ($attributeTables as $attributeTable) { + $identifiers = null; + foreach ($attributeTables as $attributeTable => $attributeIds) { $select = $connection->select() ->from( ['t' => $attributeTable], ['value' => 't.value', 'attribute_id' => 't.attribute_id'] ) - ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]); + ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) + ->where('attribute_id IN (?)', $attributeIds); + $attributeIdentifiers = []; foreach ($context as $scope) { //TODO: if (in table exists context field) $select->where( - $metadata->getEntityConnection()->quoteIdentifier($scope->getIdentifier()) . ' IN (?)', + $connection->quoteIdentifier($scope->getIdentifier()) . ' IN (?)', $this->getContextVariables($scope) - )->order('t.' . $scope->getIdentifier() . ' DESC'); + ); + $attributeIdentifiers[] = $scope->getIdentifier(); } + $attributeIdentifiers = array_unique($attributeIdentifiers); + $identifiers = array_intersect($identifiers ?? $attributeIdentifiers, $attributeIdentifiers); $selects[] = $select; } - $unionSelect = new \Magento\Framework\DB\Sql\UnionExpression( - $selects, - \Magento\Framework\DB\Select::SQL_UNION_ALL - ); - foreach ($connection->fetchAll($unionSelect) as $attributeValue) { + $this->applyIdentifierForSelects($selects, $identifiers); + $unionSelect = new UnionExpression($selects, Select::SQL_UNION_ALL, '( %s )'); + $orderedUnionSelect = $connection->select(); + $orderedUnionSelect->from(['u' => $unionSelect]); + $this->applyIdentifierForUnion($orderedUnionSelect, $identifiers); + $attributes = $connection->fetchAll($orderedUnionSelect); + foreach ($attributes as $attributeValue) { if (isset($attributesMap[$attributeValue['attribute_id']])) { $entityData[$attributesMap[$attributeValue['attribute_id']]] = $attributeValue['value']; } else { $this->logger->warning( - "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' + "Attempt to load value of nonexistent EAV attribute '{$attributeValue['attribute_id']}' for entity type '$entityType'." ); } @@ -163,4 +178,34 @@ public function execute($entityType, $entityData, $arguments = []) } return $entityData; } + + /** + * Apply identifiers column on select array. + * + * @param Select[] $selects + * @param array $identifiers + * @return void + */ + private function applyIdentifierForSelects(array $selects, array $identifiers) + { + foreach ($selects as $select) { + foreach ($identifiers as $identifier) { + $select->columns($identifier, 't'); + } + } + } + + /** + * Apply identifiers order on union select. + * + * @param Select $unionSelect + * @param array $identifiers + * @return void + */ + private function applyIdentifierForUnion(Select $unionSelect, array $identifiers) + { + foreach ($identifiers as $identifier) { + $unionSelect->order($identifier); + } + } } diff --git a/app/code/Magento/Eav/Test/Unit/Model/ResourceModel/ReadHandlerTest.php b/app/code/Magento/Eav/Test/Unit/Model/ResourceModel/ReadHandlerTest.php index 82e9033496b78..9020f22c497b6 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/ResourceModel/ReadHandlerTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/ResourceModel/ReadHandlerTest.php @@ -5,19 +5,35 @@ */ namespace Magento\Eav\Test\Unit\Model\ResourceModel; +use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; +use Magento\Eav\Model\ResourceModel\ReadHandler; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Model\Entity\ScopeResolver; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Model\Entity\ScopeInterface; +use Psr\Log\LoggerInterface; +/** + * Test for Magento\Eav\Model\ResourceModel\ReadHandler class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ReadHandlerTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject + * @var Config|\PHPUnit_Framework_MockObject_MockObject */ private $configMock; /** - * @var \Magento\Framework\EntityManager\MetadataPool|\PHPUnit_Framework_MockObject_MockObject + * @var MetadataPool|\PHPUnit_Framework_MockObject_MockObject */ private $metadataPoolMock; @@ -27,112 +43,208 @@ class ReadHandlerTest extends \PHPUnit\Framework\TestCase private $metadataMock; /** - * @var \Magento\Eav\Model\ResourceModel\ReadHandler + * @var ScopeResolver|\PHPUnit_Framework_MockObject_MockObject */ - private $readHandler; + private $scopeResolverMock; /** - * @var \Magento\Framework\Model\Entity\ScopeResolver|\PHPUnit_Framework_MockObject_MockObject + * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject */ - private $scopeResolverMock; + private $loggerMock; + + /** + * @var ScopeInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $scopeMock; + + /** + * @var AdapterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * @var AbstractAttribute|\PHPUnit_Framework_MockObject_MockObject + */ + private $attributeMock; + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var ReadHandler + */ + private $readHandler; + + /** + * @inheritdoc + */ protected function setUp() { - $objectManager = new ObjectManager($this); - $args = $objectManager->getConstructArguments(\Magento\Eav\Model\ResourceModel\ReadHandler::class); - $this->metadataPoolMock = $args['metadataPool']; - $this->metadataMock = $this->getMockBuilder(EntityMetadataInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $this->metadataPoolMock->expects($this->any()) - ->method('getMetadata') - ->willReturn($this->metadataMock); - $this->configMock = $args['config']; - $this->scopeResolverMock = $args['scopeResolver']; - $this->scopeResolverMock->method('getEntityContext') - ->willReturn([]); + $this->objectManager = new ObjectManager($this); + + $this->metadataPoolMock = $this->createMock(MetadataPool::class); + $this->scopeResolverMock = $this->createMock(ScopeResolver::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + $this->configMock = $this->createMock(Config::class); + $this->metadataMock = $this->createMock(EntityMetadataInterface::class); + $this->scopeMock = $this->createMock(ScopeInterface::class); + $this->connectionMock = $this->createMock(AdapterInterface::class); + $this->attributeMock = $this->createMock(AbstractAttribute::class); - $this->readHandler = $objectManager->getObject(\Magento\Eav\Model\ResourceModel\ReadHandler::class, $args); + $this->readHandler = $this->objectManager->getObject( + ReadHandler::class, + [ + 'metadataPool' => $this->metadataPoolMock, + 'scopeResolver' => $this->scopeResolverMock, + 'logger' => $this->loggerMock, + 'config' => $this->configMock, + ] + ); } /** - * @param string $eavEntityType - * @param int $callNum - * @param array $expected - * @param bool $isStatic - * @dataProvider executeDataProvider + * @return void */ - public function testExecute($eavEntityType, $callNum, array $expected, $isStatic = true) + public function testExecute() { + $eavEntityType = 'env-entity-type'; $entityData = ['linkField' => 'theLinkField']; - $this->metadataMock->method('getEavEntityType') - ->willReturn($eavEntityType); - $connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) - ->disableOriginalConstructor() - ->getMock(); - $selectMock->method('from') + $attributeId = '1'; + $attributeCode = 'attribute_code'; + $identifier = 'store_id'; + $expectedEntityData = [ + 'linkField' => 'theLinkField', + $attributeCode => $attributeId, + ]; + $entityDataObject = $this->objectManager->getObject(DataObject::class, ['data' => $entityData]); + $abstractBackendMock = $this->createMock(AbstractBackend::class); + $selectMock = $this->createMock(Select::class); + $orderedUnionSelectMock = $this->createMock(Select::class); + $fallbackScopeMock = $this->createMock(ScopeInterface::class); + + $this->metadataPoolMock->expects($this->exactly(2))->method('getMetadata')->willReturn($this->metadataMock); + $this->metadataMock->expects($this->exactly(2))->method('getEavEntityType')->willReturn($eavEntityType); + $this->scopeResolverMock->expects($this->once()) + ->method('getEntityContext') + ->with($eavEntityType, $entityData) + ->willReturn([$this->scopeMock]); + $this->metadataMock->expects($this->once())->method('getEntityConnection')->willReturn($this->connectionMock); + $this->configMock->expects($this->once()) + ->method('getEntityAttributes') + ->with($eavEntityType, $entityDataObject) + ->willReturn([$this->attributeMock]); + $this->attributeMock->expects($this->once())->method('isStatic')->willReturn(false); + $this->attributeMock->expects($this->once())->method('getBackend')->willReturn($abstractBackendMock); + $abstractBackendMock->expects($this->once())->method('getTable')->willReturn('some_table'); + $this->attributeMock->expects($this->exactly(2))->method('getAttributeId')->willReturn($attributeId); + $this->attributeMock->expects($this->once())->method('getAttributeCode')->willReturn($attributeCode); + $this->connectionMock->expects($this->at(0))->method('select')->willReturn($selectMock); + $selectMock->expects($this->at(0)) + ->method('from') + ->with( + [ + 't' => 'some_table', + ], + [ + 'value' => 't.value', + 'attribute_id' => 't.attribute_id', + ] + )->willReturnSelf(); + $this->metadataMock->expects($this->exactly(2))->method('getLinkField')->willReturn('linkField'); + $selectMock->expects($this->at(1)) + ->method('where') + ->with('linkField = ?', 'theLinkField') ->willReturnSelf(); - $selectMock->method('where') + $selectMock->expects($this->at(2)) + ->method('where') + ->with('attribute_id IN (?)', [$attributeId]) ->willReturnSelf(); - $connectionMock->method('select') - ->willReturn($selectMock); - $connectionMock->method('fetchAll') - ->willReturn( + $this->scopeMock->expects($this->exactly(2))->method('getIdentifier')->willReturn($identifier); + $this->connectionMock->expects($this->at(1)) + ->method('quoteIdentifier') + ->with($identifier) + ->willReturn("`$identifier`"); + $selectMock->expects($this->at(3)) + ->method('where') + ->with( + "`$identifier` IN (?)", [ - [ - 'attribute_id' => 'attributeId', - 'value' => 'attributeValue', - ] + 0 => '1', + 1 => 0, ] - ); - $this->metadataMock->method('getEntityConnection') - ->willReturn($connectionMock); - $this->metadataMock->method('getLinkField') - ->willReturn('linkField'); - - $attributeMock = $this->getMockBuilder(AbstractAttribute::class) - ->disableOriginalConstructor() - ->getMock(); - $attributeMock->method('isStatic') - ->willReturn($isStatic); - $backendMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class) - ->disableOriginalConstructor() - ->getMock(); - $backendMock->method('getTable') - ->willReturn('backendTable'); - $attributeMock->method('getBackend') - ->willReturn($backendMock); - $attributeMock->method('getAttributeId') - ->willReturn('attributeId'); - $attributeMock->method('getAttributeCode') - ->willReturn('attributeCode'); - $this->configMock->expects($this->exactly($callNum)) + )->willReturnSelf(); + $this->scopeMock->expects($this->once())->method('getValue')->willReturn('1'); + $this->scopeMock->expects($this->exactly(2))->method('getFallback')->willReturn($fallbackScopeMock); + $fallbackScopeMock->expects($this->once())->method('getValue')->willReturn(0); + $fallbackScopeMock->expects($this->once())->method('getFallback')->willReturn(null); + $selectMock->expects($this->at(4))->method('columns')->with($identifier, 't')->willReturnSelf(); + $this->connectionMock->expects($this->at(2))->method('select')->willReturn($orderedUnionSelectMock); + + $unionSelect = $this->objectManager->getObject( + UnionExpression::class, + [ + 'parts' => [$selectMock], + 'type' => 'UNION ALL', + 'pattern' => '( %s )', + ] + ); + $orderedUnionSelectMock->expects($this->once())->method('from')->with(['u' => $unionSelect])->willReturnSelf(); + $orderedUnionSelectMock->expects($this->once())->method('order')->with($identifier)->willReturnSelf(); + $this->connectionMock->expects($this->at(3)) + ->method('fetchAll') + ->with($orderedUnionSelectMock) + ->willReturn([ + [ + 'attribute_id' => '1', + 'value' => '1', + 'store_id' => '1', + ] + ]); + + $this->assertEquals($expectedEntityData, $this->readHandler->execute($eavEntityType, $entityData)); + } + + /** + * @return void + */ + public function testExecuteWithStaticAttributeAttributes() + { + $eavEntityType = 'env-entity-type'; + $entityData = ['linkField' => 'theLinkField']; + $entityDataObject = $this->objectManager->getObject(DataObject::class, ['data' => $entityData]); + $this->attributeMock = $this->createMock(AbstractAttribute::class); + + $this->metadataPoolMock->expects($this->exactly(2)) + ->method('getMetadata') + ->willReturn($this->metadataMock); + $this->metadataMock->expects($this->exactly(2))->method('getEavEntityType')->willReturn($eavEntityType); + $this->scopeResolverMock->expects($this->once()) + ->method('getEntityContext') + ->with($eavEntityType, $entityData) + ->willReturn([$this->scopeMock]); + $this->metadataMock->expects($this->once())->method('getEntityConnection')->willReturn($this->connectionMock); + $this->configMock->expects($this->once()) ->method('getEntityAttributes') - ->willReturn([$attributeMock]); - $this->assertEquals($expected, $this->readHandler->execute('entity_type', $entityData)); + ->with($eavEntityType, $entityDataObject) + ->willReturn([$this->attributeMock]); + $this->attributeMock->expects($this->once())->method('isStatic')->willReturn(true); + + $this->assertEquals($entityData, $this->readHandler->execute($eavEntityType, $entityData)); } /** - * @return array + * @return void */ - public function executeDataProvider() + public function testExecuteWithoutAttribute() { - return [ - 'null entity type' => [null, 0, ['linkField' => 'theLinkField']], - 'static attribute' => ['env-entity-type', 1, ['linkField' => 'theLinkField']], - 'non-static attribute' => [ - 'env-entity-type', - 1, - [ - 'linkField' => 'theLinkField', - 'attributeCode' => 'attributeValue' - ], - false - ], - ]; + $entityData = ['linkField' => 'theLinkField']; + + $this->metadataPoolMock->expects($this->once())->method('getMetadata')->willReturn($this->metadataMock); + $this->metadataMock->expects($this->once())->method('getEavEntityType')->willReturn(null); + + $this->assertEquals($entityData, $this->readHandler->execute('env-entity-type', $entityData)); } /** diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index c8afd10aa3eee..92c1ef11b9c1f 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -8,6 +8,7 @@ + diff --git a/app/code/Magento/Fedex/Plugin/Block/DataProviders/Tracking/ChangeTitle.php b/app/code/Magento/Fedex/Plugin/Block/DataProviders/Tracking/ChangeTitle.php new file mode 100644 index 0000000000000..b271251c4d214 --- /dev/null +++ b/app/code/Magento/Fedex/Plugin/Block/DataProviders/Tracking/ChangeTitle.php @@ -0,0 +1,34 @@ +getCarrier() === Carrier::CODE) { + $result = __('Expected Delivery:'); + } + return $result; + } +} diff --git a/app/code/Magento/Fedex/Plugin/Block/Tracking/PopupDeliveryDate.php b/app/code/Magento/Fedex/Plugin/Block/Tracking/PopupDeliveryDate.php new file mode 100644 index 0000000000000..e1597707f9d02 --- /dev/null +++ b/app/code/Magento/Fedex/Plugin/Block/Tracking/PopupDeliveryDate.php @@ -0,0 +1,54 @@ +getCarrier($subject) === Carrier::CODE) { + $result = $subject->formatDeliveryDate($date); + } + return $result; + } + + /** + * Retrieve carrier name from tracking info + * + * @param Popup $subject + * @return string + */ + private function getCarrier(Popup $subject): string + { + foreach ($subject->getTrackingInfo() as $trackingData) { + foreach ($trackingData as $trackingInfo) { + if ($trackingInfo instanceof Status) { + $carrier = $trackingInfo->getCarrier(); + return $carrier; + } + } + } + return ''; + } +} diff --git a/app/code/Magento/Fedex/etc/di.xml b/app/code/Magento/Fedex/etc/di.xml index f17f8f2afe663..c542b1f04d1eb 100644 --- a/app/code/Magento/Fedex/etc/di.xml +++ b/app/code/Magento/Fedex/etc/di.xml @@ -22,4 +22,10 @@ + + + + + + diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index 6ff2ca8c4d0eb..2d8b84cd035ff 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -626,13 +626,6 @@ public function validateSource(\Magento\ImportExport\Model\Import\AbstractSource $errorsCount = $errorAggregator->getErrorsCount(); $result = !$errorsCount; - $validationStrategy = $this->getData(self::FIELD_NAME_VALIDATION_STRATEGY); - if ($errorsCount - && $validationStrategy === ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS - ) { - $this->messageManager->addWarningMessage(__('Skipped errors: %1', $errorsCount)); - $result = true; - } if ($result) { $this->addLogComment(__('Import data validation is complete.')); diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php index 288a99770974a..179f3f3cadab0 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Export/Config/_files/invalidExportXmlArray.php @@ -22,15 +22,15 @@ 'attributes_with_type_modelName_and_invalid_value' => [ '' - . ' ', + . ' ', [ "Element 'entityType', attribute 'model': [facet 'pattern'] The value '1' is not accepted by the " . - "pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", + "pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'entityType', attribute 'model': '1' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n", - "Element 'fileFormat', attribute 'model': [facet 'pattern'] The value 'model1' is not " . - "accepted by the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", - "Element 'fileFormat', attribute 'model': 'model1' is not a valid " . + "Element 'fileFormat', attribute 'model': [facet 'pattern'] The value '1model' is not " . + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'fileFormat', attribute 'model': '1model' is not a valid " . "value of the atomic type 'modelName'.\nLine: 1\n" ], ], diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php index 357b35e8a313c..409c1af9cb38a 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportMergedXmlArray.php @@ -26,12 +26,12 @@ ["Element 'entity', attribute 'notallowed': The attribute 'notallowed' is not allowed.\nLine: 1\n"], ], 'entity_model_with_invalid_value' => [ - '', [ - "Element 'entity', attribute 'model': [facet 'pattern'] The value 'afwer34' is not " . - "accepted by the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", - "Element 'entity', attribute 'model': 'afwer34' is not a valid value of the atomic type" . + "Element 'entity', attribute 'model': [facet 'pattern'] The value '34afwer' is not " . + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'entity', attribute 'model': '34afwer' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], ], @@ -40,7 +40,7 @@ '', [ "Element 'entity', attribute 'behaviorModel': [facet 'pattern'] The value '666' is not accepted by " . - "the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", + "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'entity', attribute 'behaviorModel': '666' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php index c913b53e8b531..c7b06a8731f02 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php +++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Config/_files/invalidImportXmlArray.php @@ -19,7 +19,7 @@ '', [ "Element 'entity', attribute 'model': [facet 'pattern'] The value '12345' is not accepted by " . - "the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", + "the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'entity', attribute 'model': '12345' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -28,7 +28,7 @@ '', [ "Element 'entity', attribute 'behaviorModel': [facet 'pattern'] The value '=--09' is not " . - "accepted by the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", "Element 'entity', attribute 'behaviorModel': '=--09' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], @@ -46,11 +46,11 @@ ["Element 'entityType': The attribute 'model' is required but missing.\nLine: 1\n"], ], 'entitytype_with_invalid_model_attribute_value' => [ - '', + '', [ - "Element 'entityType', attribute 'model': [facet 'pattern'] The value 'test1' is not " . - "accepted by the pattern '[A-Za-z_\\\\]+'.\nLine: 1\n", - "Element 'entityType', attribute 'model': 'test1' is not a valid value of the atomic type" . + "Element 'entityType', attribute 'model': [facet 'pattern'] The value '1test' is not " . + "accepted by the pattern '([\\\\]?[a-zA-Z_][a-zA-Z0-9_]*)+'.\nLine: 1\n", + "Element 'entityType', attribute 'model': '1test' is not a valid value of the atomic type" . " 'modelName'.\nLine: 1\n" ], ], diff --git a/app/code/Magento/ImportExport/etc/export.xsd b/app/code/Magento/ImportExport/etc/export.xsd index 65728a9be5c62..f62dbc891ef0f 100644 --- a/app/code/Magento/ImportExport/etc/export.xsd +++ b/app/code/Magento/ImportExport/etc/export.xsd @@ -71,11 +71,11 @@ - Model name can contain only [A-Za-z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. - + diff --git a/app/code/Magento/ImportExport/etc/import.xsd b/app/code/Magento/ImportExport/etc/import.xsd index aefa6541d7e13..e73038ebc0710 100644 --- a/app/code/Magento/ImportExport/etc/import.xsd +++ b/app/code/Magento/ImportExport/etc/import.xsd @@ -61,11 +61,11 @@ - Model name can contain only [A-Za-z_\\]. + Model name can contain only ([\\]?[a-zA-Z_][a-zA-Z0-9_]*)+. - + diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 4e9b3dbdb8866..afeff3f38d63a 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -361,7 +361,7 @@ public function getLatestUpdated() return $this->getView()->getUpdated(); } } - return $this->getState()->getUpdated(); + return $this->getState()->getUpdated() ?: ''; } /** diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 6b7cc12218990..ca2da9585f934 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -164,7 +164,12 @@ public function testGetLatestUpdated($getViewIsEnabled, $getViewGetUpdated, $get } } } else { - $this->assertEquals($getStateGetUpdated, $this->model->getLatestUpdated()); + $getLatestUpdated = $this->model->getLatestUpdated(); + $this->assertEquals($getStateGetUpdated, $getLatestUpdated); + + if ($getStateGetUpdated === null) { + $this->assertNotNull($getLatestUpdated); + } } } @@ -182,7 +187,8 @@ public function getLatestUpdatedDataProvider() [true, '', '06-Jan-1944'], [true, '06-Jan-1944', ''], [true, '', ''], - [true, '06-Jan-1944', '05-Jan-1944'] + [true, '06-Jan-1944', '05-Jan-1944'], + [false, null, null], ]; } diff --git a/app/code/Magento/Msrp/etc/adminhtml/system.xml b/app/code/Magento/Msrp/etc/adminhtml/system.xml index 8ce0ea67343f8..c20d753a2e794 100644 --- a/app/code/Magento/Msrp/etc/adminhtml/system.xml +++ b/app/code/Magento/Msrp/etc/adminhtml/system.xml @@ -10,7 +10,7 @@
- + Magento\Config\Model\Config\Source\Yesno diff --git a/app/code/Magento/Msrp/i18n/en_US.csv b/app/code/Magento/Msrp/i18n/en_US.csv index d647f8527ec15..d47d72b2bdc9a 100644 --- a/app/code/Magento/Msrp/i18n/en_US.csv +++ b/app/code/Magento/Msrp/i18n/en_US.csv @@ -13,6 +13,7 @@ Price,Price "Add to Cart","Add to Cart" "Minimum Advertised Price","Minimum Advertised Price" "Enable MAP","Enable MAP" +"Warning! Enabling MAP by default will hide all product prices on Storefront.","Warning! Enabling MAP by default will hide all product prices on Storefront." "Display Actual Price","Display Actual Price" "Default Popup Text Message","Default Popup Text Message" "Default ""What's This"" Text Message","Default ""What's This"" Text Message" diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml new file mode 100644 index 0000000000000..4d63577319d5b --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/checkmo.phtml @@ -0,0 +1,26 @@ + +escapeHtml($block->getMethod()->getTitle()) ?> + {{pdf_row_separator}} +getInfo()->getAdditionalInformation()): ?> + {{pdf_row_separator}} + getPayableTo()): ?> + escapeHtml(__('Make Check payable to: %1', $block->getPayableTo())) ?> + {{pdf_row_separator}} + + getMailingAddress()): ?> + escapeHtml(__('Send Check to:')) ?> + {{pdf_row_separator}} + escapeHtml($block->getMailingAddress())) ?> + {{pdf_row_separator}} + + diff --git a/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml new file mode 100644 index 0000000000000..4a6ea1c00b21c --- /dev/null +++ b/app/code/Magento/OfflinePayments/view/base/templates/info/pdf/purchaseorder.phtml @@ -0,0 +1,11 @@ + +escapeHtml(__('Purchase Order Number: %1', $block->getInfo()->getPoNumber())) ?> + {{pdf_row_separator}} diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php new file mode 100644 index 0000000000000..7a1cc8934c017 --- /dev/null +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance.php @@ -0,0 +1,108 @@ +cacheManager = $cacheManager; + $this->pageCacheStateStorage = $pageCacheStateStorage; + } + + /** + * Switches Full Page Cache. + * + * Depending on enabling or disabling Maintenance Mode it turns off or restores Full Page Cache state. + * + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer) + { + if ($observer->getData('isOn')) { + $this->pageCacheStateStorage->save($this->isFullPageCacheEnabled()); + $this->turnOffFullPageCache(); + } else { + $this->restoreFullPageCacheState(); + } + } + + /** + * Turns off Full Page Cache. + * + * @return void + */ + private function turnOffFullPageCache() + { + if (!$this->isFullPageCacheEnabled()) { + return; + } + + $this->cacheManager->clean([PageCacheType::TYPE_IDENTIFIER]); + $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], false); + } + + /** + * Full Page Cache state. + * + * @return bool + */ + private function isFullPageCacheEnabled(): bool + { + $cacheStatus = $this->cacheManager->getStatus(); + + if (!array_key_exists(PageCacheType::TYPE_IDENTIFIER, $cacheStatus)) { + return false; + } + + return (bool)$cacheStatus[PageCacheType::TYPE_IDENTIFIER]; + } + + /** + * Restores Full Page Cache state. + * + * Returns FPC to previous state that was before maintenance mode turning on. + * + * @return void + */ + private function restoreFullPageCacheState() + { + $storedPageCacheState = $this->pageCacheStateStorage->isEnabled(); + $this->pageCacheStateStorage->flush(); + + if ($storedPageCacheState) { + $this->cacheManager->setEnabled([PageCacheType::TYPE_IDENTIFIER], true); + } + } +} diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php new file mode 100644 index 0000000000000..4180885fcbc54 --- /dev/null +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php @@ -0,0 +1,74 @@ +flagDir = $fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + } + + /** + * Saves Full Page Cache state. + * + * Saves FPC state across requests. + * + * @param bool $state + * @return void + */ + public function save(bool $state) + { + $this->flagDir->writeFile(self::PAGE_CACHE_STATE_FILENAME, (string)$state); + } + + /** + * Returns stored Full Page Cache state. + * + * @return bool + */ + public function isEnabled(): bool + { + if (!$this->flagDir->isExist(self::PAGE_CACHE_STATE_FILENAME)) { + return false; + } + + return (bool)$this->flagDir->readFile(self::PAGE_CACHE_STATE_FILENAME); + } + + /** + * Flushes Page Cache state storage. + * + * @return void + */ + public function flush() + { + $this->flagDir->delete(self::PAGE_CACHE_STATE_FILENAME); + } +} diff --git a/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php new file mode 100644 index 0000000000000..8c4661cddd44c --- /dev/null +++ b/app/code/Magento/PageCache/Test/Unit/Observer/SwitchPageCacheOnMaintenanceTest.php @@ -0,0 +1,161 @@ +cacheManager = $this->createMock(Manager::class); + $this->pageCacheStateStorage = $this->createMock(PageCacheState::class); + $this->observer = $this->createMock(Observer::class); + + $this->model = $objectManager->getObject(SwitchPageCacheOnMaintenance::class, [ + 'cacheManager' => $this->cacheManager, + 'pageCacheStateStorage' => $this->pageCacheStateStorage, + ]); + } + + /** + * Tests execute when setting maintenance mode to on. + * + * @param array $cacheStatus + * @param bool $cacheState + * @param int $flushCacheCalls + * @return void + * @dataProvider enablingPageCacheStateProvider + */ + public function testExecuteWhileMaintenanceEnabling(array $cacheStatus, bool $cacheState, int $flushCacheCalls) + { + $this->observer->method('getData') + ->with('isOn') + ->willReturn(true); + $this->cacheManager->method('getStatus') + ->willReturn($cacheStatus); + + // Page Cache state will be stored. + $this->pageCacheStateStorage->expects($this->once()) + ->method('save') + ->with($cacheState); + + // Page Cache will be cleaned and disabled + $this->cacheManager->expects($this->exactly($flushCacheCalls)) + ->method('clean') + ->with([PageCacheType::TYPE_IDENTIFIER]); + $this->cacheManager->expects($this->exactly($flushCacheCalls)) + ->method('setEnabled') + ->with([PageCacheType::TYPE_IDENTIFIER], false); + + $this->model->execute($this->observer); + } + + /** + * Tests execute when setting Maintenance Mode to off. + * + * @param bool $storedCacheState + * @param int $enableCacheCalls + * @return void + * @dataProvider disablingPageCacheStateProvider + */ + public function testExecuteWhileMaintenanceDisabling(bool $storedCacheState, int $enableCacheCalls) + { + $this->observer->method('getData') + ->with('isOn') + ->willReturn(false); + + $this->pageCacheStateStorage->method('isEnabled') + ->willReturn($storedCacheState); + + // Nullify Page Cache state. + $this->pageCacheStateStorage->expects($this->once()) + ->method('flush'); + + // Page Cache will be enabled. + $this->cacheManager->expects($this->exactly($enableCacheCalls)) + ->method('setEnabled') + ->with([PageCacheType::TYPE_IDENTIFIER]); + + $this->model->execute($this->observer); + } + + /** + * Page Cache state data provider. + * + * @return array + */ + public function enablingPageCacheStateProvider(): array + { + return [ + 'page_cache_is_enable' => [ + 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 1], + 'cache_state' => true, + 'flush_cache_calls' => 1, + ], + 'page_cache_is_missing_in_system' => [ + 'cache_status' => [], + 'cache_state' => false, + 'flush_cache_calls' => 0, + ], + 'page_cache_is_disable' => [ + 'cache_status' => [PageCacheType::TYPE_IDENTIFIER => 0], + 'cache_state' => false, + 'flush_cache_calls' => 0, + ], + ]; + } + + /** + * Page Cache state data provider. + * + * @return array + */ + public function disablingPageCacheStateProvider(): array + { + return [ + ['stored_cache_state' => true, 'enable_cache_calls' => 1], + ['stored_cache_state' => false, 'enable_cache_calls' => 0], + ]; + } +} diff --git a/app/code/Magento/PageCache/etc/events.xml b/app/code/Magento/PageCache/etc/events.xml index 7584f5f36d69c..3f0a2532ae60a 100644 --- a/app/code/Magento/PageCache/etc/events.xml +++ b/app/code/Magento/PageCache/etc/events.xml @@ -57,4 +57,7 @@ + + + diff --git a/app/code/Magento/Payment/Model/CcConfigProvider.php b/app/code/Magento/Payment/Model/CcConfigProvider.php index 15bdd0072a51a..497ce93c30c71 100644 --- a/app/code/Magento/Payment/Model/CcConfigProvider.php +++ b/app/code/Magento/Payment/Model/CcConfigProvider.php @@ -44,7 +44,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getConfig() { @@ -69,7 +69,7 @@ public function getIcons() } $types = $this->ccConfig->getCcAvailableTypes(); - foreach (array_keys($types) as $code) { + foreach ($types as $code => $label) { if (!array_key_exists($code, $this->icons)) { $asset = $this->ccConfig->createAsset('Magento_Payment::images/cc/' . strtolower($code) . '.png'); $placeholder = $this->assetSource->findSource($asset); @@ -78,7 +78,8 @@ public function getIcons() $this->icons[$code] = [ 'url' => $asset->getUrl(), 'width' => $width, - 'height' => $height + 'height' => $height, + 'title' => __($label), ]; } } diff --git a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php index a8856166995fc..ff6aea44645cf 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/CcConfigProviderTest.php @@ -42,12 +42,14 @@ public function testGetConfig() 'vi' => [ 'url' => 'http://cc.card/vi.png', 'width' => getimagesize($imagesDirectoryPath . 'vi.png')[0], - 'height' => getimagesize($imagesDirectoryPath . 'vi.png')[1] + 'height' => getimagesize($imagesDirectoryPath . 'vi.png')[1], + 'title' => __('Visa'), ], 'ae' => [ 'url' => 'http://cc.card/ae.png', 'width' => getimagesize($imagesDirectoryPath . 'ae.png')[0], - 'height' => getimagesize($imagesDirectoryPath . 'ae.png')[1] + 'height' => getimagesize($imagesDirectoryPath . 'ae.png')[1], + 'title' => __('American Express'), ] ] ] @@ -56,11 +58,13 @@ public function testGetConfig() $ccAvailableTypesMock = [ 'vi' => [ + 'title' => 'Visa', 'fileId' => 'Magento_Payment::images/cc/vi.png', 'path' => $imagesDirectoryPath . 'vi.png', 'url' => 'http://cc.card/vi.png' ], 'ae' => [ + 'title' => 'American Express', 'fileId' => 'Magento_Payment::images/cc/ae.png', 'path' => $imagesDirectoryPath . 'ae.png', 'url' => 'http://cc.card/ae.png' @@ -68,7 +72,11 @@ public function testGetConfig() ]; $assetMock = $this->createMock(\Magento\Framework\View\Asset\File::class); - $this->ccConfigMock->expects($this->once())->method('getCcAvailableTypes')->willReturn($ccAvailableTypesMock); + $this->ccConfigMock->expects($this->once())->method('getCcAvailableTypes') + ->willReturn(array_combine( + array_keys($ccAvailableTypesMock), + array_column($ccAvailableTypesMock, 'title') + )); $this->ccConfigMock->expects($this->atLeastOnce()) ->method('createAsset') diff --git a/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml new file mode 100644 index 0000000000000..7acac62f65d38 --- /dev/null +++ b/app/code/Magento/Payment/view/base/templates/info/pdf/default.phtml @@ -0,0 +1,23 @@ + +escapeHtml($block->getMethod()->getTitle()) ?>{{pdf_row_separator}} + +getSpecificInformation()):?> + $value):?> + escapeHtml($label) ?>: + escapeHtml(implode(' ', $block->getValueAsArray($value))) ?> + {{pdf_row_separator}} + + + +escapeHtml(implode('{{pdf_row_separator}}', $block->getChildPdfAsArray())) ?> diff --git a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php index 2efae34a96459..497e32157de05 100644 --- a/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php +++ b/app/code/Magento/Paypal/Controller/Transparent/RequestSecureToken.php @@ -11,6 +11,7 @@ use Magento\Framework\Controller\ResultInterface; use Magento\Framework\Session\Generic; use Magento\Framework\Session\SessionManager; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Paypal\Model\Payflow\Service\Request\SecureToken; use Magento\Paypal\Model\Payflow\Transparent; use Magento\Quote\Model\Quote; @@ -39,7 +40,7 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action private $secureTokenService; /** - * @var SessionManager + * @var SessionManager|SessionManagerInterface */ private $sessionManager; @@ -55,6 +56,7 @@ class RequestSecureToken extends \Magento\Framework\App\Action\Action * @param SecureToken $secureTokenService * @param SessionManager $sessionManager * @param Transparent $transparent + * @param SessionManagerInterface|null $sessionInterface */ public function __construct( Context $context, @@ -62,12 +64,13 @@ public function __construct( Generic $sessionTransparent, SecureToken $secureTokenService, SessionManager $sessionManager, - Transparent $transparent + Transparent $transparent, + SessionManagerInterface $sessionInterface = null ) { $this->resultJsonFactory = $resultJsonFactory; $this->sessionTransparent = $sessionTransparent; $this->secureTokenService = $secureTokenService; - $this->sessionManager = $sessionManager; + $this->sessionManager = $sessionInterface ?: $sessionManager; $this->transparent = $transparent; parent::__construct($context); } @@ -82,7 +85,7 @@ public function execute() /** @var Quote $quote */ $quote = $this->sessionManager->getQuote(); - if (!$quote or !$quote instanceof Quote) { + if (!$quote || !$quote instanceof Quote) { return $this->getErrorResponse(); } @@ -106,6 +109,8 @@ public function execute() } /** + * Get error response. + * * @return Json */ private function getErrorResponse() diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index a3f5d1aaa6a6a..6ea70e63fdbf6 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -1354,7 +1354,7 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add } /** - * Retrieve quote items collection + * Retrieve quote items collection. * * @param bool $useCache * @return \Magento\Eav\Model\Entity\Collection\AbstractCollection @@ -1362,10 +1362,10 @@ public function addShippingAddress(\Magento\Quote\Api\Data\AddressInterface $add */ public function getItemsCollection($useCache = true) { - if ($this->hasItemsCollection()) { + if ($this->hasItemsCollection() && $useCache) { return $this->getData('items_collection'); } - if (null === $this->_items) { + if (null === $this->_items || !$useCache) { $this->_items = $this->_quoteItemCollectionFactory->create(); $this->extensionAttributesJoinProcessor->process($this->_items); $this->_items->setQuote($this); diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index e2ee8bbad01b9..e21ae7fa1af37 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -25,6 +25,7 @@ /** * Class QuoteManagement * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -355,6 +356,13 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) if ($quote->getCheckoutMethod() === self::METHOD_GUEST) { $quote->setCustomerId(null); $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); + if ($quote->getCustomerFirstname() === null && $quote->getCustomerLastname() === null) { + $quote->setCustomerFirstname($quote->getBillingAddress()->getFirstname()); + $quote->setCustomerLastname($quote->getBillingAddress()->getLastname()); + if ($quote->getCustomerMiddlename() === null) { + $quote->setCustomerMiddlename($quote->getBillingAddress()->getMiddlename()); + } + } $quote->setCustomerIsGuest(true); $quote->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); } diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php new file mode 100644 index 0000000000000..0ac589b11b53b --- /dev/null +++ b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php @@ -0,0 +1,71 @@ +quoteRepository = $quoteRepository; + $this->checkoutSession = $checkoutSession; + } + + /** + * Update store id in active quote after store view switching. + * + * @param StoreSwitcherInterface $subject + * @param string $result + * @param StoreInterface $fromStore store where we came from + * @param StoreInterface $targetStore store where to go to + * @param string $redirectUrl original url requested for redirect after switching + * @return string url to be redirected after switching + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSwitch( + StoreSwitcherInterface $subject, + string $result, + StoreInterface $fromStore, + StoreInterface $targetStore, + string $redirectUrl + ): string { + $quote = $this->checkoutSession->getQuote(); + if ($quote->getIsActive()) { + $quote->setStoreId($targetStore->getId()); + $quote->getItemsCollection(false); + $this->quoteRepository->save($quote); + } + + return $result; + } +} diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index a2f08353a4f3b..e5e7c3834bf7d 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -22,6 +22,9 @@ + + + @@ -70,7 +73,10 @@ + + + @@ -79,13 +85,11 @@ - - @@ -94,8 +98,6 @@ - - @@ -113,10 +115,37 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index 145a18fb34ca3..cdfd0df8f9927 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -642,7 +642,7 @@ public function testPlaceOrderIfCustomerIsGuest() $addressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, ['getEmail']); $addressMock->expects($this->once())->method('getEmail')->willReturn($email); - $this->quoteMock->expects($this->once())->method('getBillingAddress')->with()->willReturn($addressMock); + $this->quoteMock->expects($this->any())->method('getBillingAddress')->with()->willReturn($addressMock); $this->quoteMock->expects($this->once())->method('setCustomerIsGuest')->with(true)->willReturnSelf(); $this->quoteMock->expects($this->once()) diff --git a/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php b/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php new file mode 100644 index 0000000000000..f6146c824aabc --- /dev/null +++ b/app/code/Magento/Quote/Test/Unit/Plugin/UpdateQuoteItemStoreTest.php @@ -0,0 +1,143 @@ +checkoutSessionMock = $this->createPartialMock( + Session::class, + ['getQuote'] + ); + $this->quoteMock = $this->createPartialMock( + Quote::class, + ['getIsActive', 'setStoreId', 'getItemsCollection'] + ); + $this->storeMock = $this->createPartialMock( + Store::class, + ['getId'] + ); + $this->quoteRepositoryMock = $this->createPartialMock( + QuoteRepository::class, + ['save'] + ); + $this->subjectMock = $this->createMock(StoreSwitcherInterface::class); + + $this->checkoutSessionMock->expects($this->once())->method('getQuote')->willReturn($this->quoteMock); + + $this->model = $objectManager->getObject( + UpdateQuoteItemStore::class, + [ + 'quoteRepository' => $this->quoteRepositoryMock, + 'checkoutSession' => $this->checkoutSessionMock, + ] + ); + } + + /** + * Unit test for afterSwitch method with active quote. + * + * @return void + */ + public function testWithActiveQuote() + { + $storeId = 1; + $this->quoteMock->expects($this->once())->method('getIsActive')->willReturn(true); + $this->storeMock->expects($this->once())->method('getId')->willReturn($storeId); + $this->quoteMock->expects($this->once())->method('setStoreId')->with($storeId)->willReturnSelf(); + $quoteItem = $this->createMock(Item::class); + $this->quoteMock->expects($this->once())->method('getItemsCollection')->willReturnSelf($quoteItem); + + $this->model->afterSwitch( + $this->subjectMock, + 'magento2.loc', + $this->storeMock, + $this->storeMock, + 'magento2.loc' + ); + } + + /** + * Unit test for afterSwitch method without active quote. + * + * @dataProvider getIsActive + * @param bool|null $isActive + * @return void + */ + public function testWithoutActiveQuote($isActive) + { + $this->quoteMock->expects($this->once())->method('getIsActive')->willReturn($isActive); + $this->quoteRepositoryMock->expects($this->never())->method('save'); + + $this->model->afterSwitch( + $this->subjectMock, + 'magento2.loc', + $this->storeMock, + $this->storeMock, + 'magento2.loc' + ); + } + + /** + * Data provider for method testWithoutActiveQuote. + * @return array + */ + public function getIsActive() + { + return [ + [false], + [null], + ]; + } +} diff --git a/app/code/Magento/Quote/etc/frontend/di.xml b/app/code/Magento/Quote/etc/frontend/di.xml index 25acd6763ba56..91f4cfbf60aba 100644 --- a/app/code/Magento/Quote/etc/frontend/di.xml +++ b/app/code/Magento/Quote/etc/frontend/di.xml @@ -12,6 +12,9 @@ Magento\Checkout\Model\Session\Proxy + + + diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php index 1f90309721c23..9c80f6aa423b8 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales/Grid.php @@ -6,23 +6,58 @@ namespace Magento\Reports\Block\Adminhtml\Sales\Sales; +use Magento\Framework\DataObject; use Magento\Reports\Block\Adminhtml\Grid\Column\Renderer\Currency; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Model\Order\ConfigFactory; +use Magento\Sales\Model\Order; /** * Adminhtml sales report grid block * - * @author Magento Core Team * @SuppressWarnings(PHPMD.DepthOfInheritance) */ class Grid extends \Magento\Reports\Block\Adminhtml\Grid\AbstractGrid { /** - * GROUP BY criteria - * * @var string */ protected $_columnGroupBy = 'period'; + /** + * @var ConfigFactory + */ + private $configFactory; + + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Backend\Helper\Data $backendHelper + * @param \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory + * @param \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory + * @param \Magento\Reports\Helper\Data $reportsData + * @param array $data + * @param ConfigFactory|null $configFactory + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Backend\Helper\Data $backendHelper, + \Magento\Reports\Model\ResourceModel\Report\Collection\Factory $resourceFactory, + \Magento\Reports\Model\Grouped\CollectionFactory $collectionFactory, + \Magento\Reports\Helper\Data $reportsData, + array $data = [], + ConfigFactory $configFactory = null + ) { + parent::__construct( + $context, + $backendHelper, + $resourceFactory, + $collectionFactory, + $reportsData, + $data + ); + $this->configFactory = $configFactory ?: ObjectManager::getInstance()->get(ConfigFactory::class); + } + /** * {@inheritdoc} * @codeCoverageIgnore @@ -328,4 +363,31 @@ protected function _prepareColumns() return parent::_prepareColumns(); } + + /** + * @inheritdoc + * + * Filter canceled statuses for orders. + * + * @return Grid + */ + protected function _prepareCollection() + { + /** @var DataObject $filterData */ + $filterData = $this->getData('filter_data'); + if (!$filterData->hasData('order_statuses')) { + $orderConfig = $this->configFactory->create(); + $statusValues = []; + $canceledStatuses = $orderConfig->getStateStatuses(Order::STATE_CANCELED); + $statusCodes = array_keys($orderConfig->getStatuses()); + foreach ($statusCodes as $code) { + if (!isset($canceledStatuses[$code])) { + $statusValues[] = $code; + } + } + $filterData->setData('order_statuses', $statusValues); + } + + return parent::_prepareCollection(); + } } diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index fd9adbe734101..da7ab97a1b211 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -445,7 +445,7 @@ public function getDateRange($range, $customStart, $customEnd, $returnObjects = break; case 'custom': - $dateStart = $customStart ? $customStart : $dateEnd; + $dateStart = $customStart ? $customStart : $dateStart; $dateEnd = $customEnd ? $customEnd : $dateEnd; break; diff --git a/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml new file mode 100644 index 0000000000000..8ddfd4092645f --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/ActionGroup/GenerateOrderReportActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml b/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml new file mode 100644 index 0000000000000..f0b51f6e39357 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Page/AdminOrdersReportPage.xml @@ -0,0 +1,14 @@ + + + + +
+
+
+ + diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml new file mode 100644 index 0000000000000..33527e1262020 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportFilterSection.xml @@ -0,0 +1,16 @@ + + + + +
+ + + +
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml new file mode 100644 index 0000000000000..c4a96537740ee --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportMainActionsSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml new file mode 100644 index 0000000000000..e920d28bcf386 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Section/AdminOrderReportTableSection.xml @@ -0,0 +1,15 @@ + + + + +
+ + +
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml new file mode 100644 index 0000000000000..009e4b8e5f6f1 --- /dev/null +++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + <description value="Verify canceling of orders in order sales report"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13838"/> + <useCaseId value="MAGETWO-95463"/> + </annotations> + <before> + <!-- create new product --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- create new customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!--login to Admin--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="logout" stepKey="logout"/> + </after> + + <!--Create new order--> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="startToCreateNewOrder"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrderWithUserDefinedQty"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping"/> + <!--Select payment--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment"/> + <!--Submit Order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeSuccessMessage"/> + <!--Create order invoice--> + <comment userInput="Admin creates invoice for order" stepKey="adminCreateInvoiceComment" /> + <actionGroup ref="StartCreateInvoiceFromOrderPage" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoice" stepKey="submitInvoice"/> + <!--Ship Order--> + <comment userInput="Admin creates shipment" stepKey="adminCreatesShipmentComment"/> + <actionGroup ref="StartCreateShipmentFromOrderPage" stepKey="createShipment"/> + <actionGroup ref="SubmitShipment" stepKey="submitShipment"/> + + <!--Create new order--> + <comment userInput="Admin creates order" stepKey="adminCreateOrderComment1"/> + <actionGroup ref="navigateToNewOrderPageExistingCustomer" stepKey="startToCreateNewOrder1"> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="addSimpleProductToOrder" stepKey="addSimpleProductToOrderWithUserDefinedQty1"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="quantity" value="1"/> + </actionGroup> + <!-- Select shipping --> + <actionGroup ref="orderSelectFlatRateShipping" stepKey="selectFlatRateShipping1"/> + <!--Select payment--> + <actionGroup ref="SelectCheckMoneyPaymentMethod" stepKey="selectCheckMoneyPayment1"/> + <!--Submit Order--> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder1"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeSuccessMessage1"/> + <!-- Cancel order --> + <actionGroup ref="cancelPendingOrder" stepKey="cancelOrder"/> + + <!-- Generate Order report --> + <amOnPage url="{{AdminOrdersReportPage.url}}" stepKey="goToAdminOrdersReportPage"/> + <!-- Get date --> + <generateDate date="+0 day" format="m/d/Y" stepKey="generateEndDate"/> + <generateDate date="-1 day" format="m/d/Y" stepKey="generateStartDate"/> + <actionGroup ref="GenerateOrderReportActionGroup" stepKey="generateReportAfterCancelOrder"> + <argument name="orderFromDate" value="$generateStartDate"/> + <argument name="orderToDate" value="$generateEndDate"/> + </actionGroup> + <waitForElement selector="{{AdminOrderReportTableSection.ordersCount}}" stepKey="waitForOrdersCount"/> + <see selector="{{AdminOrderReportTableSection.canceledOrders}}" userInput="$0.00" stepKey="seeCanceledOrderPrice"/> + </test> +</tests> diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index d6868eae6fcbc..9331a3d4610e0 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Block\Adminhtml; /** @@ -56,6 +57,7 @@ public function __construct( * * @return void * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) */ protected function _construct() { @@ -159,13 +161,13 @@ protected function _construct() } if ($this->getRequest()->getParam('ret', false) == 'pending') { - $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('catalog/*/pending') . '\')'); + $this->buttonList->update('back', 'onclick', 'setLocation(\'' . $this->getUrl('review/*/pending') . '\')'); $this->buttonList->update( 'delete', 'onclick', 'deleteConfirm(' . '\'' . __( 'Are you sure you want to do this?' - ) . '\' ' . '\'' . $this->getUrl( + ) . '\', ' . '\'' . $this->getUrl( '*/*/delete', [$this->_objectId => $this->getRequest()->getParam($this->_objectId), 'ret' => 'pending'] ) . '\'' . ')' diff --git a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php index 7159b1825dc4d..857f36b19a19c 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Product/Save.php @@ -9,9 +9,14 @@ use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Exception\LocalizedException; +/** + * Save Review action. + */ class Save extends ProductController { /** + * Save Review action. + * * @return \Magento\Backend\Model\View\Result\Redirect * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -63,7 +68,7 @@ public function execute() if ($nextId) { $resultRedirect->setPath('review/*/edit', ['id' => $nextId]); } elseif ($this->getRequest()->getParam('ret') == 'pending') { - $resultRedirect->setPath('*/*/pending'); + $resultRedirect->setPath('review/*/pending'); } else { $resultRedirect->setPath('*/*/'); } diff --git a/app/code/Magento/Review/etc/acl.xml b/app/code/Magento/Review/etc/acl.xml index 397cc1cce61d6..09b80750da14d 100644 --- a/app/code/Magento/Review/etc/acl.xml +++ b/app/code/Magento/Review/etc/acl.xml @@ -17,7 +17,7 @@ <resource id="Magento_Backend::marketing"> <resource id="Magento_Backend::marketing_user_content"> <resource id="Magento_Review::reviews_all" title="Reviews" translate="title" sortOrder="10"/> - <resource id="Magento_Review::pending" title="Reviews" translate="title" sortOrder="20"/> + <resource id="Magento_Review::pending" title="Pending Reviews" translate="title" sortOrder="20"/> </resource> </resource> </resource> diff --git a/app/code/Magento/Review/etc/adminhtml/menu.xml b/app/code/Magento/Review/etc/adminhtml/menu.xml index e3532483f88af..8b56f36bce68e 100644 --- a/app/code/Magento/Review/etc/adminhtml/menu.xml +++ b/app/code/Magento/Review/etc/adminhtml/menu.xml @@ -9,6 +9,7 @@ <menu> <add id="Magento_Review::catalog_reviews_ratings_ratings" title="Rating" translate="title" module="Magento_Review" sortOrder="60" parent="Magento_Backend::stores_attributes" action="review/rating/" resource="Magento_Review::ratings"/> <add id="Magento_Review::catalog_reviews_ratings_reviews_all" title="Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="10" action="review/product/index" resource="Magento_Review::reviews_all"/> + <add id="Magento_Review::catalog_reviews_ratings_pending" title="Pending Reviews" translate="title" module="Magento_Review" parent="Magento_Backend::marketing_user_content" sortOrder="15" action="review/product/pending" resource="Magento_Review::pending"/> <add id="Magento_Review::report_review" title="Reviews" translate="title" module="Magento_Reports" sortOrder="20" parent="Magento_Reports::report" resource="Magento_Reports::review"/> <add id="Magento_Review::report_review_customer" title="By Customers" translate="title" sortOrder="10" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/customer" resource="Magento_Reports::review_customer"/> <add id="Magento_Review::report_review_product" title="By Products" translate="title" sortOrder="20" module="Magento_Review" parent="Magento_Review::report_review" action="reports/report_review/product" resource="Magento_Reports::review_product"/> diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index 83c21d6602fca..4c80588d814f6 100644 --- a/app/code/Magento/Robots/Model/Config/Value.php +++ b/app/code/Magento/Robots/Model/Config/Value.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Robots\Model\Config; use Magento\Framework\App\Cache\TypeListInterface; @@ -30,12 +31,11 @@ class Value extends ConfigValue implements IdentityInterface const CACHE_TAG = 'robots'; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [self::CACHE_TAG]; /** * @var StoreResolver diff --git a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php index c02bbd64e7ca3..53ba319d47ef0 100644 --- a/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php +++ b/app/code/Magento/Rule/Model/Condition/Product/AbstractProduct.php @@ -95,8 +95,8 @@ abstract class AbstractProduct extends \Magento\Rule\Model\Condition\AbstractCon * @param \Magento\Catalog\Model\ResourceModel\Product $productResource * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attrSetCollection * @param \Magento\Framework\Locale\FormatInterface $localeFormat - * @param ProductCategoryList|null $categoryList * @param array $data + * @param ProductCategoryList|null $categoryList * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -137,7 +137,6 @@ public function getDefaultOperatorInputByType() */ $this->_defaultOperatorInputByType['category'] = ['==', '!=', '{}', '!{}', '()', '!()']; $this->_arrayInputTypes[] = 'category'; - $this->_defaultOperatorInputByType['sku'] = ['==', '!=', '{}', '!{}', '()', '!()']; } return $this->_defaultOperatorInputByType; } @@ -383,9 +382,6 @@ public function getInputType() if ($this->getAttributeObject()->getAttributeCode() == 'category_ids') { return 'category'; } - if ($this->getAttributeObject()->getAttributeCode() == 'sku') { - return 'sku'; - } switch ($this->getAttributeObject()->getFrontendInput()) { case 'select': return 'select'; @@ -520,6 +516,10 @@ public function loadArray($arr) ) ? $this->_localeFormat->getNumber( $arr['is_value_parsed'] ) : false; + } elseif (!empty($arr['operator']) && $arr['operator'] == '()') { + if (isset($arr['value'])) { + $arr['value'] = preg_replace('/\s*,\s*/', ',', $arr['value']); + } } return parent::loadArray($arr); @@ -610,10 +610,6 @@ public function getBindArgumentValue() $this->getValueParsed() )->__toString() ); - } elseif ($this->getAttribute() === 'sku') { - $value = $this->getData('value'); - $value = preg_split('#\s*[,;]\s*#', $value, null, PREG_SPLIT_NO_EMPTY); - $this->setValueParsed($value); } return parent::getBindArgumentValue(); @@ -706,6 +702,7 @@ protected function _getAttributeSetId($productId) /** * Correct '==' and '!=' operators + * * Categories can't be equal because product is included categories selected by administrator and in their parents * * @return string @@ -713,7 +710,7 @@ protected function _getAttributeSetId($productId) public function getOperatorForValidate() { $operator = $this->getOperator(); - if (in_array($this->getInputType(), ['category', 'sku'])) { + if ('category' === $this->getInputType()) { if ($operator == '==') { $operator = '{}'; } elseif ($operator == '!=') { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php index b61a5cf83734b..11f3faa9d042e 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php @@ -11,7 +11,6 @@ use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\Pricing\PriceCurrencyInterface; -use Magento\Store\Model\ScopeInterface; /** * Create order account form @@ -135,8 +134,9 @@ protected function _prepareForm() $this->_addAttributesToForm($attributes, $fieldset); $this->_form->addFieldNameSuffix('order[account]'); - $storeId = (int)$this->_sessionQuote->getStoreId(); - $this->_form->setValues($this->extractValuesFromAttributes($attributes, $storeId)); + + $formValues = $this->extractValuesFromAttributes($attributes); + $this->_form->setValues($formValues); return $this; } @@ -188,10 +188,9 @@ public function getFormValues() * Extract the form values from attributes. * * @param array $attributes - * @param int $storeId * @return array */ - private function extractValuesFromAttributes(array $attributes, int $storeId): array + private function extractValuesFromAttributes(array $attributes): array { $formValues = $this->getFormValues(); foreach ($attributes as $code => $attribute) { @@ -199,26 +198,8 @@ private function extractValuesFromAttributes(array $attributes, int $storeId): a if (isset($defaultValue) && !isset($formValues[$code])) { $formValues[$code] = $defaultValue; } - if ($code === 'group_id' && empty($defaultValue)) { - $formValues[$code] = $this->getDefaultCustomerGroup($storeId); - } } return $formValues; } - - /** - * Gets default customer group. - * - * @param int $storeId - * @return string|null - */ - private function getDefaultCustomerGroup(int $storeId) - { - return $this->_scopeConfig->getValue( - 'customer/create_account/default_group', - ScopeInterface::SCOPE_STORE, - $storeId - ); - } } diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 12d2a5396ddc4..99393f77397c1 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -24,6 +24,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 100.0.2 */ class Create extends \Magento\Framework\DataObject implements \Magento\Checkout\Model\Cart\CartInterface @@ -583,6 +584,7 @@ public function initFromOrder(\Magento\Sales\Model\Order $order) } $quote->getShippingAddress()->unsCachedItemsAll(); + $quote->getBillingAddress()->unsCachedItemsAll(); $quote->setTotalsCollectedFlag(false); $this->quoteRepository->save($quote); diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 9d66433be8f3e..f153e8362e6f9 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -1298,12 +1298,12 @@ public function getTrackingNumbers() * Retrieve shipping method * * @param bool $asObject return carrier code and shipping method data as object - * @return string|\Magento\Framework\DataObject + * @return string|null|\Magento\Framework\DataObject */ public function getShippingMethod($asObject = false) { $shippingMethod = parent::getShippingMethod(); - if (!$asObject) { + if (!$asObject || !$shippingMethod) { return $shippingMethod; } else { list($carrierCode, $method) = explode('_', $shippingMethod, 2); diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php index f06da0de0fd00..7ac11ca073d26 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php @@ -97,7 +97,7 @@ public function __construct( */ public function send(Order $order, $forceSyncMode = false) { - $order->setSendEmail(true); + $order->setSendEmail($this->identityContainer->isEnabled()); if (!$this->globalConfig->getValue('sales_email/general/async_sending') || $forceSyncMode) { if ($this->checkAndSend($order)) { diff --git a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php index 7ec089b882972..5fb89b7855056 100644 --- a/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php +++ b/app/code/Magento/Sales/Model/Order/Email/SenderBuilder.php @@ -5,7 +5,6 @@ */ namespace Magento\Sales\Model\Order\Email; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Mail\Template\TransportBuilderByStore; use Magento\Sales\Model\Order\Email\Container\IdentityInterface; @@ -29,11 +28,8 @@ class SenderBuilder protected $transportBuilder; /** - * @var TransportBuilderByStore - */ - private $transportBuilderByStore; - - /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * * @param Template $templateContainer * @param IdentityInterface $identityContainer * @param TransportBuilder $transportBuilder @@ -48,9 +44,6 @@ public function __construct( $this->templateContainer = $templateContainer; $this->identityContainer = $identityContainer; $this->transportBuilder = $transportBuilder; - $this->transportBuilderByStore = $transportBuilderByStore ?: ObjectManager::getInstance()->get( - TransportBuilderByStore::class - ); } /** @@ -110,7 +103,7 @@ protected function configureEmailTemplate() $this->transportBuilder->setTemplateIdentifier($this->templateContainer->getTemplateId()); $this->transportBuilder->setTemplateOptions($this->templateContainer->getTemplateOptions()); $this->transportBuilder->setTemplateVars($this->templateContainer->getTemplateVars()); - $this->transportBuilderByStore->setFromByStore( + $this->transportBuilder->setFromByScope( $this->identityContainer->getEmailIdentity(), $this->identityContainer->getStore()->getId() ); diff --git a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php index 9857fa39fa51a..80e909941c5ce 100644 --- a/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php +++ b/app/code/Magento/Sales/Observer/AssignOrderToCustomerObserver.php @@ -57,6 +57,17 @@ public function execute(Observer $observer) $orderId = $delegateData['__sales_assign_order_id']; $order = $this->orderRepository->get($orderId); if (!$order->getCustomerId() && $customer->getId()) { + // Assign customer info to order after customer creation. + $order->setCustomerId($customer->getId()) + ->setCustomerIsGuest(0) + ->setCustomerEmail($customer->getEmail()) + ->setCustomerFirstname($customer->getFirstname()) + ->setCustomerLastname($customer->getLastname()) + ->setCustomerMiddlename($customer->getMiddlename()) + ->setCustomerPrefix($customer->getPrefix()) + ->setCustomerSuffix($customer->getSuffix()) + ->setCustomerGroupId($customer->getGroupId()); + $this->assignmentService->execute($order, $customer); } } diff --git a/app/code/Magento/Sales/Setup/UpgradeData.php b/app/code/Magento/Sales/Setup/UpgradeData.php index 77b96791e8cea..2e5a454e62fdd 100644 --- a/app/code/Magento/Sales/Setup/UpgradeData.php +++ b/app/code/Magento/Sales/Setup/UpgradeData.php @@ -15,6 +15,7 @@ use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\UpgradeDataInterface; use Magento\Quote\Model\QuoteFactory; +use Magento\Sales\Model\Order\Address; use Magento\Sales\Model\OrderFactory; use Magento\Sales\Model\ResourceModel\Order\Address\CollectionFactory as AddressCollectionFactory; @@ -42,27 +43,14 @@ class UpgradeData implements UpgradeDataInterface */ private $aggregatedFieldConverter; - /** - * @var AddressCollectionFactory - */ - private $addressCollectionFactory; - - /** - * @var OrderFactory - */ - private $orderFactory; - - /** - * @var QuoteFactory - */ - private $quoteFactory; - /** * @var State */ private $state; /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * * @param SalesSetupFactory $salesSetupFactory * @param Config $eavConfig * @param AggregatedFieldDataConverter $aggregatedFieldConverter @@ -83,9 +71,6 @@ public function __construct( $this->salesSetupFactory = $salesSetupFactory; $this->eavConfig = $eavConfig; $this->aggregatedFieldConverter = $aggregatedFieldConverter; - $this->addressCollectionFactory = $addressCollFactory; - $this->orderFactory = $orderFactory; - $this->quoteFactory = $quoteFactory; $this->state = $state; } @@ -125,6 +110,7 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface * @param string $setupVersion * @param SalesSetup $salesSetup * @return void + * @throws \Magento\Framework\DB\FieldDataConversionException */ private function convertSerializedDataToJson($setupVersion, SalesSetup $salesSetup) { @@ -173,32 +159,95 @@ private function convertSerializedDataToJson($setupVersion, SalesSetup $salesSet /** * Fill quote_address_id in table sales_order_address if it is empty. - * * @param ModuleDataSetupInterface $setup */ public function fillQuoteAddressIdInSalesOrderAddress(ModuleDataSetupInterface $setup) { - $addressTable = $setup->getTable('sales_order_address'); - $updateOrderAddress = $setup->getConnection() + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_SHIPPING); + $this->fillQuoteAddressIdInSalesOrderAddressByType($setup, Address::TYPE_BILLING); + } + + /** + * @param ModuleDataSetupInterface $setup + * @param string $addressType + */ + private function fillQuoteAddressIdInSalesOrderAddressByType(ModuleDataSetupInterface $setup, $addressType) + { + $salesConnection = $setup->getConnection('sales'); + + $orderTable = $setup->getTable('sales_order', 'sales'); + $orderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $salesConnection ->select() + ->from( + ['sales_order_address' => $orderAddressTable], + ['entity_id', 'address_type'] + ) ->joinInner( - ['sales_order' => $setup->getTable('sales_order')], - $addressTable . '.parent_id = sales_order.entity_id', - ['quote_address_id' => 'quote_address.address_id'] + ['sales_order' => $orderTable], + 'sales_order_address.parent_id = sales_order.entity_id', + ['quote_id' => 'sales_order.quote_id'] + ) + ->where('sales_order_address.quote_address_id IS NULL') + ->where('sales_order_address.address_type = ?', $addressType) + ->order('sales_order_address.entity_id'); + + $batchSize = 5000; + $result = $salesConnection->query($query); + $count = $result->rowCount(); + $batches = ceil($count / $batchSize); + + for ($batch = $batches; $batch > 0; $batch--) { + $query->limitPage($batch, $batchSize); + $result = $salesConnection->fetchAssoc($query); + + $this->fillQuoteAddressIdInSalesOrderAddressProcessBatch($setup, $result, $addressType); + } + } + + /** + * @param ModuleDataSetupInterface $setup + * @param array $orderAddresses + * @param string $addressType + */ + private function fillQuoteAddressIdInSalesOrderAddressProcessBatch( + ModuleDataSetupInterface $setup, + array $orderAddresses, + $addressType + ) { + $salesConnection = $setup->getConnection('sales'); + $quoteConnection = $setup->getConnection('checkout'); + + $quoteAddressTable = $setup->getTable('quote_address', 'checkout'); + $quoteTable = $setup->getTable('quote', 'checkout'); + $salesOrderAddressTable = $setup->getTable('sales_order_address', 'sales'); + + $query = $quoteConnection + ->select() + ->from( + ['quote_address' => $quoteAddressTable], + ['quote_id', 'address_id'] ) ->joinInner( - ['quote_address' => $setup->getTable('quote_address')], - 'sales_order.quote_id = quote_address.quote_id - AND ' . $addressTable . '.address_type = quote_address.address_type', + ['quote' => $quoteTable], + 'quote_address.quote_id = quote.entity_id', [] ) - ->where( - $addressTable . '.quote_address_id IS NULL' - ); - $updateOrderAddress = $setup->getConnection()->updateFromSelect( - $updateOrderAddress, - $addressTable - ); - $setup->getConnection()->query($updateOrderAddress); + ->where('quote.entity_id in (?)', array_column($orderAddresses, 'quote_id')) + ->where('address_type = ?', $addressType); + + $quoteAddresses = $quoteConnection->fetchAssoc($query); + + foreach ($orderAddresses as $orderAddress) { + $bind = [ + 'quote_address_id' => $quoteAddresses[$orderAddress['quote_id']]['address_id'] ?? null, + ]; + $where = [ + 'entity_id = ?' => $orderAddress['entity_id'] + ]; + + $salesConnection->update($salesOrderAddressTable, $bind, $where); + } } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml new file mode 100644 index 0000000000000..de1ed79db81ad --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminCreditMemoActionGroup.xml @@ -0,0 +1,28 @@ +<?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="StartCreateCreditMemoFromOrderPage"> + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreditMemoButton"/> + <conditionalClick selector="{{AdminConfirmationModalSection.ok}}" dependentSelector="{{AdminConfirmationModalSection.ok}}" + visible="true" stepKey="acceptModal"/> + <waitForPageLoad time="30" stepKey="waitPageLoaded"/> + <seeInCurrentUrl url="{{AdminCreditMemoNewPage.url}}" stepKey="seeNewCreditMemoUrl"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewCreditMemoPageTitle"/> + </actionGroup> + + <actionGroup name="SubmitCreditMemo"> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="waitButtonEnabled"/> + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickSubmitCreditMemo"/> + <waitForElementVisible selector="{{AdminMessagesSection.successMessage}}" stepKey="waitForMessageAppears"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="You created the credit memo." stepKey="seeCreditMemoCreateSuccess"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> + <seeInCurrentUrl url="{{AdminOrderDetailsPage.url('$grabOrderId')}}" stepKey="seeViewOrderPageCreditMemo"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index 730d3d6a5a185..479b631db0b71 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -180,6 +180,51 @@ <fillField selector="{{AdminOrderFormBillingAddressSection.postalCode}}" userInput="{{address.postcode}}" stepKey="fillPostalCode"/> <fillField selector="{{AdminOrderFormBillingAddressSection.phone}}" userInput="{{address.telephone}}" stepKey="fillPhone"/> </actionGroup> + <!--Check customer billing address fields--> + <actionGroup name="CheckOrderCustomerBillingInformation"> + <arguments> + <argument name="customer"/> + <argument name="address"/> + </arguments> + <seeInField selector="{{AdminOrderFormBillingAddressSection.firstName}}" userInput="{{customer.firstname}}" stepKey="checkFirstName"/> + <seeInField selector="{{AdminOrderFormBillingAddressSection.lastName}}" userInput="{{customer.lastname}}" stepKey="checkLastName"/> + <seeInField selector="{{AdminOrderFormBillingAddressSection.streetLine1}}" userInput="{{address.street[0]}}" stepKey="checkStreet"/> + <seeInField selector="{{AdminOrderFormBillingAddressSection.city}}" userInput="{{address.city}}" stepKey="checkCity"/> + <seeInField selector="{{AdminOrderFormBillingAddressSection.country}}" userInput="{{address.country_id}}" stepKey="checkCountry"/> + <seeInField selector="{{AdminOrderFormBillingAddressSection.state}}" userInput="{{address.state}}" stepKey="checkState"/> + <seeInField selector="{{AdminOrderFormBillingAddressSection.postalCode}}" userInput="{{address.postcode}}" stepKey="checkPostCode"/> + <seeInField selector="{{AdminOrderFormBillingAddressSection.phone}}" userInput="{{address.telephone}}" stepKey="checkTelephone"/> + </actionGroup> + <!--Fill customer shipping address--> + <actionGroup name="FillOrderCustomerShippingInformation"> + <arguments> + <argument name="customer"/> + <argument name="address"/> + </arguments> + <fillField selector="{{AdminOrderFormShippingAddressSection.firstName}}" userInput="{{customer.firstname}}" stepKey="fillFirstName"/> + <fillField selector="{{AdminOrderFormShippingAddressSection.lastName}}" userInput="{{customer.lastname}}" stepKey="fillLastName"/> + <fillField selector="{{AdminOrderFormShippingAddressSection.streetLine1}}" userInput="{{address.street[0]}}" stepKey="fillStreetLine1"/> + <fillField selector="{{AdminOrderFormShippingAddressSection.city}}" userInput="{{address.city}}" stepKey="fillCity"/> + <selectOption selector="{{AdminOrderFormShippingAddressSection.country}}" userInput="{{address.country_id}}" stepKey="fillCountry"/> + <selectOption selector="{{AdminOrderFormShippingAddressSection.state}}" userInput="{{address.state}}" stepKey="fillState"/> + <fillField selector="{{AdminOrderFormShippingAddressSection.postalCode}}" userInput="{{address.postcode}}" stepKey="fillPostalCode"/> + <fillField selector="{{AdminOrderFormShippingAddressSection.phone}}" userInput="{{address.telephone}}" stepKey="fillPhone"/> + </actionGroup> + <!--Check customer shipping address fields--> + <actionGroup name="CheckOrderCustomerShippingInformation"> + <arguments> + <argument name="customer"/> + <argument name="address"/> + </arguments> + <seeInField selector="{{AdminOrderFormShippingAddressSection.firstName}}" userInput="{{customer.firstname}}" stepKey="checkFirstName"/> + <seeInField selector="{{AdminOrderFormShippingAddressSection.lastName}}" userInput="{{customer.lastname}}" stepKey="checkLastName"/> + <seeInField selector="{{AdminOrderFormShippingAddressSection.streetLine1}}" userInput="{{address.street[0]}}" stepKey="checkStreet"/> + <seeInField selector="{{AdminOrderFormShippingAddressSection.city}}" userInput="{{address.city}}" stepKey="checkCity"/> + <seeInField selector="{{AdminOrderFormShippingAddressSection.country}}" userInput="{{address.country_id}}" stepKey="checkCountry"/> + <seeInField selector="{{AdminOrderFormShippingAddressSection.state}}" userInput="{{address.state}}" stepKey="checkState"/> + <seeInField selector="{{AdminOrderFormShippingAddressSection.postalCode}}" userInput="{{address.postcode}}" stepKey="checkPostCode"/> + <seeInField selector="{{AdminOrderFormShippingAddressSection.phone}}" userInput="{{address.telephone}}" stepKey="checkTelephone"/> + </actionGroup> <!--Select flat rate shipping method--> <actionGroup name="orderSelectFlatRateShipping"> <click selector="{{AdminOrderFormPaymentSection.header}}" stepKey="unfocus"/> @@ -251,4 +296,10 @@ <waitForElementVisible selector="{{AdminOrderFormPaymentSection.paymentBlock}}" stepKey="waitForPaymentOptions"/> <conditionalClick selector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" dependentSelector="{{AdminOrderFormPaymentSection.checkMoneyOption}}" visible="true" stepKey="checkCheckMoneyOption"/> </actionGroup> + <!--Submit order--> + <actionGroup name="SubmitOrderActionGroup"> + <click selector="{{AdminOrderFormActionSection.submitOrder}}" stepKey="clickSubmitOrder"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeOrderSuccessMessage"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml index e67a2c3b961d9..ca4239bb060ca 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminOrderCreatePage.xml @@ -7,12 +7,13 @@ --> <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminOrderCreatePage" url="sales/order_create/index" area="admin" module="Magento_Sales"> <section name="AdminOrderFormSelectWebsiteSection"/> <section name="AdminOrderFormActionSection"/> <section name="AdminOrderFormAccountSection"/> <section name="AdminOrderFormBillingAddressSection"/> + <section name="AdminOrderFormShippingAddressSection"/> <section name="AdminOrderFormPaymentSection"/> <section name="AdminOrderFormItemsSection"/> <section name="AdminOrderFormTotalSection"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml index 496fae2b548a7..15d4df39416f2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoItemsSection.xml @@ -10,6 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreditMemoItemsSection"> <element name="itemQtyToRefund" type="input" selector=".order-creditmemo-tables tbody:nth-of-type({{row}}) .col-refund .qty-input" parameterized="true"/> - <element name="updateQty" type="button" selector=".order-creditmemo-tables tfoot button[data-ui-id='order-items-update-button']" timeout="30"/> + <element name="updateQty" type="button" selector=".order-creditmemo-tables tfoot button[data-ui-id='order-items-update-button']:not([class~='disabled'])" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml index 648f519455b56..e087873fe62c2 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml @@ -11,8 +11,8 @@ <section name="AdminCreditMemoTotalSection"> <element name="total" type="text" selector="//table[contains(@class,'order-subtotal-table')]/tbody/tr/td[contains(text(), '{{total}}')]/following-sibling::td/span/span[contains(@class, 'price')]" parameterized="true"/> <element name="refundShipping" type="input" selector=".order-subtotal-table tbody input[name='creditmemo[shipping_amount]']"/> - <element name="updateTotals" type="button" selector=".update-totals-button" timeout="30"/> - <element name="submitRefundOffline" type="button" selector=".order-totals-actions button[title='Refund Offline']" timeout="30"/> - <element name="submitRefundOfflineEnabled" type="button" selector=".order-totals-actions button[data-ui-id='order-items-submit-button'][class='action-default scalable save submit-button primary']" timeout="30"/> + <element name="updateTotals" type="button" selector="button[class~='update-totals-button']:not([class~='disabled'])" timeout="30"/> + <element name="submitRefundOffline" type="button" selector=".order-totals-actions button[title='Refund Offline']:not([class~='disabled'])" timeout="30"/> + <element name="adjustmentRefund" type="input" selector=".order-subtotal-table tbody input[name='creditmemo[adjustment_positive]']"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml index eac238584b030..3794def0ac77a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderDetailsMainActionsSection"> <element name="back" type="button" selector="#back" timeout="30"/> <element name="cancel" type="button" selector="#order-view-cancel-button" timeout="30"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml index 83d417f6f8555..efffa48103b93 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormConfigureProductSection.xml @@ -9,8 +9,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminOrderFormConfigureProductSection"> - <element name="optionSelect" type="select" selector="//div[@class='product-options']/div/div/select[../../label[text() = '{{option}}']]" parameterized="true"/> + <element name="optionSelect" type="select" selector="//div[contains(@class, 'product-options')]//div//label[.='{{option}}']//following-sibling::*//select" parameterized="true"/> <element name="quantity" type="input" selector="#product_composite_configure_input_qty"/> <element name="ok" type="button" selector=".modal-header .page-actions button[data-role='action']" timeout="30"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml new file mode 100644 index 0000000000000..aae90589a390f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormShippingAddressSection.xml @@ -0,0 +1,23 @@ +<?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="AdminOrderFormShippingAddressSection"> + <element name="firstName" type="input" selector="#order-shipping_address_firstname" timeout="30"/> + <element name="lastName" type="input" selector="#order-shipping_address_lastname" timeout="30"/> + <element name="streetLine1" type="input" selector="#order-shipping_address_street0" timeout="30"/> + <element name="city" type="input" selector="#order-shipping_address_city" timeout="30"/> + <element name="country" type="select" selector="#order-shipping_address_country_id" timeout="30"/> + <element name="state" type="select" selector="#order-shipping_address_region_id" timeout="30"/> + <element name="postalCode" type="input" selector="#order-shipping_address_postcode" timeout="30"/> + <element name="phone" type="input" selector="#order-shipping_address_telephone" timeout="30"/> + <element name="saveAddress" type="checkbox" selector="#order-shipping_address_save_in_address_book"/> + <element name="sameAsBilling" type="checkbox" selector="#order-shipping_same_as_billing"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderViewTabsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderViewTabsSection.xml index 78d0a45bce460..0037c482fcd72 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderViewTabsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderViewTabsSection.xml @@ -12,5 +12,6 @@ <element name="invoices" type="button" selector="#sales_order_view_tabs_order_invoices"/> <element name="creditMemos" type="button" selector="#sales_order_view_tabs_order_creditmemos"/> <element name="shipments" type="button" selector="#sales_order_view_tabs_order_shipments"/> + <element name="commentsHistory" type="button" selector="#sales_order_view_tabs_order_history" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml index cb761dc358abb..5c0ebe99287b5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAbleToShipPartiallyInvoicedItemsTest.xml @@ -14,9 +14,6 @@ <severity value="CRITICAL"/> <testCaseId value="MAGETWO-95524"/> <group value="sales"/> - <skip> - <issueId value="MAGETWO-97664"/> - </skip> </annotations> <before> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -119,7 +116,7 @@ <!--Submit refund--> <scrollTo selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" stepKey="scrollToItemsToRefund"/> <seeInField selector="{{AdminCreditMemoItemsSection.itemQtyToRefund('1')}}" userInput="5" after="scrollToItemsToRefund" stepKey="checkQtyOfItemsToRefund"/> - <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOfflineEnabled}}" stepKey="waitForSubmitRefundOfflineEnabled"/> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="waitForSubmitRefundOfflineEnabled"/> <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="submitRefundOffline"/> <!--Verify Credit Memo created successfully--> <waitForElementVisible diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml index b73c66d49d690..e9fff90bdd632 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCheckingCreditMemoTotalsTest.xml @@ -91,15 +91,10 @@ <click selector="{{AdminOrderInvoiceViewMainActionsSection.creditMemo}}" stepKey="clickCreditMemo"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoPageTitle"/> <fillField selector="{{AdminCreditMemoTotalSection.refundShipping}}" userInput="0" stepKey="setRefundShipping"/> + <waitForElementVisible selector="{{AdminCreditMemoTotalSection.updateTotals}}" time="30" stepKey="waitUpdateTotalsButtonEnabled"/> <click selector="{{AdminCreditMemoTotalSection.updateTotals}}" stepKey="clickUpdateTotals"/> <waitForLoadingMaskToDisappear stepKey="waitForUpdateTotals"/> - <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickRefundOffline"/> - <waitForElementVisible - selector="{{AdminMessagesSection.success}}" - time="10" - stepKey="waitForMessage"/> - <see selector="{{AdminMessagesSection.success}}" userInput="You created the credit memo." - stepKey="seeCreatedCreditMemoSuccessMessage"/> + <actionGroup ref="SubmitCreditMemo" stepKey="submitCreditMemo"/> <!--Go to Credit Memo tab--> <click selector="{{AdminOrderViewTabsSection.creditMemos}}" stepKey="clickCreditMemosTab"/> diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php index 46c44c03b1514..88053ea684ce8 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/OrderSenderTest.php @@ -64,7 +64,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen $this->orderMock->expects($this->once()) ->method('setSendEmail') - ->with(true); + ->with($emailSendingResult); $this->globalConfig->expects($this->once()) ->method('getValue') @@ -72,7 +72,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen ->willReturn($configValue); if (!$configValue || $forceSyncMode) { - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn($emailSendingResult); @@ -118,7 +118,7 @@ public function testSend($configValue, $forceSyncMode, $emailSendingResult, $sen $this->orderMock->expects($this->once()) ->method('setEmailSent') - ->with(true); + ->with($emailSendingResult); $this->orderResourceMock->expects($this->once()) ->method('saveAttribute') @@ -210,7 +210,7 @@ public function testSendVirtualOrder($isVirtualOrder, $formatCallCount, $expecte ->with('sales_email/general/async_sending') ->willReturn(false); - $this->identityContainerMock->expects($this->once()) + $this->identityContainerMock->expects($this->exactly(2)) ->method('isEnabled') ->willReturn(true); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php index 38209bb22aef4..24cd54e3a46b3 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/SenderBuilderTest.php @@ -6,7 +6,6 @@ namespace Magento\Sales\Test\Unit\Model\Order\Email; -use Magento\Framework\Mail\Template\TransportBuilderByStore; use Magento\Sales\Model\Order\Email\SenderBuilder; class SenderBuilderTest extends \PHPUnit\Framework\TestCase @@ -36,11 +35,6 @@ class SenderBuilderTest extends \PHPUnit\Framework\TestCase */ private $storeMock; - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $transportBuilderByStore; - protected function setUp() { $templateId = 'test_template_id'; @@ -82,11 +76,10 @@ protected function setUp() 'setTemplateIdentifier', 'setTemplateOptions', 'setTemplateVars', + 'setFromByScope', ] ); - $this->transportBuilderByStore = $this->createMock(TransportBuilderByStore::class); - $this->templateContainerMock->expects($this->once()) ->method('getTemplateId') ->will($this->returnValue($templateId)); @@ -109,9 +102,9 @@ protected function setUp() $this->identityContainerMock->expects($this->once()) ->method('getEmailIdentity') ->will($this->returnValue($emailIdentity)); - $this->transportBuilderByStore->expects($this->once()) - ->method('setFromByStore') - ->with($this->equalTo($emailIdentity)); + $this->transportBuilder->expects($this->once()) + ->method('setFromByScope') + ->with($this->equalTo($emailIdentity), 1); $this->identityContainerMock->expects($this->once()) ->method('getEmailCopyTo') @@ -120,8 +113,7 @@ protected function setUp() $this->senderBuilder = new SenderBuilder( $this->templateContainerMock, $this->identityContainerMock, - $this->transportBuilder, - $this->transportBuilderByStore + $this->transportBuilder ); } @@ -129,6 +121,8 @@ public function testSend() { $customerName = 'test_name'; $customerEmail = 'test_email'; + $identity = 'email_identity_test'; + $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class ); @@ -151,6 +145,9 @@ public function testSend() $this->storeMock->expects($this->once()) ->method('getId') ->willReturn(1); + $this->transportBuilder->expects($this->once()) + ->method('setFromByScope') + ->with($identity, 1); $this->transportBuilder->expects($this->once()) ->method('addTo') ->with($this->equalTo($customerEmail), $this->equalTo($customerName)); @@ -164,6 +161,7 @@ public function testSend() public function testSendCopyTo() { + $identity = 'email_identity_test'; $transportMock = $this->createMock( \Magento\Sales\Test\Unit\Model\Order\Email\Stub\TransportInterfaceMock::class ); @@ -177,6 +175,9 @@ public function testSendCopyTo() $this->transportBuilder->expects($this->once()) ->method('addTo') ->with($this->equalTo('example@mail.com')); + $this->transportBuilder->expects($this->once()) + ->method('setFromByScope') + ->with($identity, 1); $this->identityContainerMock->expects($this->once()) ->method('getStore') ->willReturn($this->storeMock); diff --git a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php index 18371274049e3..8890f01130c82 100644 --- a/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php +++ b/app/code/Magento/Sales/Test/Unit/Observer/AssignOrderToCustomerObserverTest.php @@ -52,9 +52,10 @@ protected function setUp() * * @dataProvider getCustomerIds * @param null|int $customerId + * @param null|int $customerOrderId * @return void */ - public function testAssignOrderToCustomerAfterGuestOrder($customerId) + public function testAssignOrderToCustomerAfterGuestOrder($customerId, $customerOrderId) { $orderId = 1; /** @var Observer|PHPUnit_Framework_MockObject_MockObject $observerMock */ @@ -64,7 +65,12 @@ public function testAssignOrderToCustomerAfterGuestOrder($customerId) ->setMethods(['getData']) ->getMock(); /** @var CustomerInterface|PHPUnit_Framework_MockObject_MockObject $customerMock */ - $customerMock = $this->createMock(CustomerInterface::class); + $customerMock = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $customerMock->expects($this->any()) + ->method('getId') + ->willReturn($customerId); /** @var OrderInterface|PHPUnit_Framework_MockObject_MockObject $orderMock */ $orderMock = $this->getMockBuilder(OrderInterface::class) ->disableOriginalConstructor() @@ -75,13 +81,24 @@ public function testAssignOrderToCustomerAfterGuestOrder($customerId) ['delegate_data', null, ['__sales_assign_order_id' => $orderId]], ['customer_data_object', null, $customerMock] ]); - $orderMock->expects($this->once())->method('getCustomerId')->willReturn($customerId); + $orderMock->expects($this->any())->method('getCustomerId')->willReturn($customerOrderId); $this->orderRepositoryMock->expects($this->once())->method('get')->with($orderId) ->willReturn($orderMock); - if ($customerId) { + if (!$customerOrderId && $customerId) { + $orderMock->expects($this->once())->method('setCustomerId')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerIsGuest')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerEmail')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerFirstname')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerLastname')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerMiddlename')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerPrefix')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerSuffix')->willReturn($orderMock); + $orderMock->expects($this->once())->method('setCustomerGroupId')->willReturn($orderMock); + $this->assignmentMock->expects($this->once())->method('execute')->with($orderMock, $customerMock); $this->sut->execute($observerMock); + return; } @@ -94,8 +111,12 @@ public function testAssignOrderToCustomerAfterGuestOrder($customerId) * * @return array */ - public function getCustomerIds() + public function getCustomerIds(): array { - return [[null, 1]]; + return [ + [null, null], + [1, null], + [1, 1], + ]; } } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml index a73740c249b67..b0b52b199978a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml @@ -140,67 +140,76 @@ </section> <script> -require(['jquery', 'prototype'], function(jQuery){ +require(['jquery'], function(jQuery){ //<![CDATA[ -var submitButtons = $$('.submit-button'); -var updateButtons = $$('.update-button,.update-totals-button'); -var fields = $$('.qty-input,.order-subtotal-table input[type="text"]'); +var submitButtons = jQuery('.submit-button'); +var updateButtons = jQuery('.update-button,.update-totals-button'); +var fields = jQuery('.qty-input,.order-subtotal-table input[type="text"]'); -updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); +function enableButtons(buttons) { + buttons.removeClass('disabled').prop('disabled', false); +}; -for(var i=0;i<fields.length;i++){ - fields[i].observe('change', checkButtonsRelation) - fields[i].baseValue = fields[i].value; -} +function disableButtons(buttons) { + buttons.addClass('disabled').prop('disabled', true); +}; + +disableButtons(updateButtons); + +fields.on('change', checkButtonsRelation); +fields.each(function (i, elem) { + elem.baseValue = elem.value; +}); function checkButtonsRelation() { var hasChanges = false; - fields.each(function (elem) { + fields.each(function (i, elem) { if (elem.baseValue != elem.value) { hasChanges = true; } }.bind(this)); if (hasChanges) { - submitButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + disableButtons(submitButtons); + enableButtons(updateButtons); } else { - submitButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); + enableButtons(submitButtons); + disableButtons(updateButtons); } } submitCreditMemo = function() { - if ($('creditmemo_do_offline')) $('creditmemo_do_offline').value=0; + var creditMemoOffline = jQuery('#creditmemo_do_offline'); + if (creditMemoOffline.length) { + creditMemoOffline.prop('value', 0); + } // Temporary solution will be replaced after refactoring order functionality jQuery('#edit_form').triggerHandler('save'); } submitCreditMemoOffline = function() { - if ($('creditmemo_do_offline')) $('creditmemo_do_offline').value=1; + var creditMemoOffline = jQuery('#creditmemo_do_offline'); + if (creditMemoOffline.length) { + creditMemoOffline.prop('value', 1); + } // Temporary solution will be replaced after refactoring order functionality jQuery('#edit_form').triggerHandler('save'); } -var sendEmailCheckbox = $('send_email'); - -if (sendEmailCheckbox) { - var notifyCustomerCheckbox = $('notify_customer'); - var creditmemoCommentText = $('creditmemo_comment_text'); - Event.observe(sendEmailCheckbox, 'change', bindSendEmail); +var sendEmailCheckbox = jQuery('#send_email'); +if (sendEmailCheckbox.length) { + var notifyCustomerCheckbox = jQuery('#notify_customer'); + sendEmailCheckbox.on('change', bindSendEmail); bindSendEmail(); } -function bindSendEmail() -{ - if (sendEmailCheckbox.checked == true) { - notifyCustomerCheckbox.disabled = false; - //creditmemoCommentText.disabled = false; +function bindSendEmail() { + if (sendEmailCheckbox.prop('checked') == true) { + notifyCustomerCheckbox.prop('disabled', false); } else { - notifyCustomerCheckbox.disabled = true; - //creditmemoCommentText.disabled = true; + notifyCustomerCheckbox.prop('disabled', true); } } diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml index 4a77c3b166de9..0d873645bce86 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml @@ -134,56 +134,61 @@ </section> <script> -require(['jquery', 'prototype'], function(jQuery){ +require(['jquery'], function(jQuery){ //<![CDATA[ -var submitButtons = $$('.submit-button'); -var updateButtons = $$('.update-button'); +var submitButtons = jQuery('.submit-button'); +var updateButtons = jQuery('.update-button'); var enableSubmitButtons = <?= (int) !$block->getDisableSubmitButton() ?>; -var fields = $$('.qty-input'); +var fields = jQuery('.qty-input'); -updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); +function enableButtons(buttons) { + buttons.removeClass('disabled').prop('disabled', false); +}; -for(var i=0;i<fields.length;i++){ - jQuery(fields[i]).on('keyup', checkButtonsRelation); - fields[i].baseValue = fields[i].value; -} +function disableButtons(buttons) { + buttons.addClass('disabled').prop('disabled', true); +}; + +disableButtons(updateButtons); + +fields.on('keyup', checkButtonsRelation); +fields.each(function (i, elem) { + elem.baseValue = elem.value; +}); function checkButtonsRelation() { var hasChanges = false; - fields.each(function (elem) { + fields.each(function (i, elem) { if (elem.baseValue != elem.value) { hasChanges = true; } }.bind(this)); if (hasChanges) { - submitButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); - updateButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + disableButtons(submitButtons); + enableButtons(updateButtons); } else { if (enableSubmitButtons) { - submitButtons.each(function (elem) {elem.disabled=false;elem.removeClassName('disabled');}); + enableButtons(submitButtons); } - updateButtons.each(function (elem) {elem.disabled=true;elem.addClassName('disabled');}); + disableButtons(updateButtons); } } -var sendEmailCheckbox = $('send_email'); -if (sendEmailCheckbox) { - var notifyCustomerCheckbox = $('notify_customer'); - var invoiceCommentText = $('invoice_comment_text'); - Event.observe(sendEmailCheckbox, 'change', bindSendEmail); +var sendEmailCheckbox = jQuery('#send_email'); +if (sendEmailCheckbox.length) { + var notifyCustomerCheckbox = jQuery('#notify_customer'); + sendEmailCheckbox.on('change', bindSendEmail); bindSendEmail(); } function bindSendEmail() { - if (sendEmailCheckbox.checked == true) { - notifyCustomerCheckbox.disabled = false; - //invoiceCommentText.disabled = false; + if (sendEmailCheckbox.prop('checked') == true) { + notifyCustomerCheckbox.prop('disabled', false); } else { - notifyCustomerCheckbox.disabled = true; - //invoiceCommentText.disabled = true; + notifyCustomerCheckbox.prop('disabled', true); } } diff --git a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml index 9b3633fde60b4..a2ab3d02b13ea 100644 --- a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml @@ -57,7 +57,7 @@ </button> </div> <div class="secondary"> - <a class="action view" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/account') ?>"> + <a class="action view" href="<?= /* @escapeNotVerified */ $block->getUrl('customer/account') ?>#my-orders-table"> <span><?= /* @escapeNotVerified */ __('View All') ?></span> </a> </div> diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php index 5e6f3847c8e31..423cd1543117b 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule/Collection.php @@ -9,6 +9,9 @@ use Magento\Framework\DB\Select; use Magento\Framework\Serialize\Serializer\Json; use Magento\Quote\Model\Quote\Address; +use Magento\SalesRule\Api\Data\CouponInterface; +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\Rule; /** * Sales Rules resource collection model. @@ -107,12 +110,15 @@ protected function mapAssociatedEntities($entityType, $objectField) $associatedEntities = $this->getConnection()->fetchAll($select); - array_map(function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { - $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); - $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); - $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; - $item->setData($objectField, $itemAssociatedValue); - }, $associatedEntities); + array_map( + function ($associatedEntity) use ($entityInfo, $ruleIdField, $objectField) { + $item = $this->getItemByColumnValue($ruleIdField, $associatedEntity[$ruleIdField]); + $itemAssociatedValue = $item->getData($objectField) === null ? [] : $item->getData($objectField); + $itemAssociatedValue[] = $associatedEntity[$entityInfo['entity_id_field']]; + $item->setData($objectField, $itemAssociatedValue); + }, + $associatedEntities + ); } /** @@ -141,6 +147,7 @@ protected function _afterLoad() * @param string $couponCode * @param string|null $now * @param Address $address allow extensions to further filter out rules based on quote address + * @throws \Zend_Db_Select_Exception * @use $this->addWebsiteGroupDateFilter() * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return $this @@ -153,32 +160,24 @@ public function setValidationFilter( Address $address = null ) { if (!$this->getFlag('validation_filter')) { - /* We need to overwrite joinLeft if coupon is applied */ - $this->getSelect()->reset(); - parent::_initSelect(); + $this->prepareSelect($websiteId, $customerGroupId, $now); - $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); - $select = $this->getSelect(); + $noCouponRules = $this->getNoCouponCodeSelect(); - $connection = $this->getConnection(); - if (strlen($couponCode)) { - $noCouponWhereCondition = $connection->quoteInto( - 'main_table.coupon_type = ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); - $relatedRulesIds = $this->getCouponRelatedRuleIds($couponCode); - - $select->where( - $noCouponWhereCondition . ' OR main_table.rule_id IN (?)', - $relatedRulesIds, - Select::TYPE_CONDITION - ); + if ($couponCode) { + $couponRules = $this->getCouponCodeSelect($couponCode); + + $allAllowedRules = $this->getConnection()->select(); + $allAllowedRules->union([$noCouponRules, $couponRules], Select::SQL_UNION_ALL); + + $wrapper = $this->getConnection()->select(); + $wrapper->from($allAllowedRules); + + $this->_select = $wrapper; } else { - $this->addFieldToFilter( - 'main_table.coupon_type', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON - ); + $this->_select = $noCouponRules; } + $this->setOrder('sort_order', self::SORT_ORDER_ASC); $this->setFlag('validation_filter', true); } @@ -187,72 +186,98 @@ public function setValidationFilter( } /** - * Get rules ids related to coupon code + * Recreate the default select object for specific needs of salesrule evaluation with coupon codes. * - * @param string $couponCode - * @return array + * @param int $websiteId + * @param int $customerGroupId + * @param string $now */ - private function getCouponRelatedRuleIds(string $couponCode): array + private function prepareSelect($websiteId, $customerGroupId, $now) { - $connection = $this->getConnection(); - $select = $connection->select()->from( - ['main_table' => $this->getTable('salesrule')], - 'rule_id' + $this->getSelect()->reset(); + parent::_initSelect(); + + $this->addWebsiteGroupDateFilter($websiteId, $customerGroupId, $now); + } + + /** + * Return select object to determine all active rules not needing a coupon code. + * + * @return Select + */ + private function getNoCouponCodeSelect() + { + $noCouponSelect = clone $this->getSelect(); + + $noCouponSelect->where( + 'main_table.coupon_type = ?', + Rule::COUPON_TYPE_NO_COUPON ); - $select->joinLeft( - ['rule_coupons' => $this->getTable('salesrule_coupon')], - $connection->quoteInto( - 'main_table.rule_id = rule_coupons.rule_id AND main_table.coupon_type != ?', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON, - null - ) + + $noCouponSelect->columns([Coupon::KEY_CODE => new \Zend_Db_Expr('NULL')]); + + return $noCouponSelect; + } + + /** + * Determine all active rules that are valid for the given coupon code. + * + * @param string $couponCode + * @return Select + */ + private function getCouponCodeSelect($couponCode) + { + $couponSelect = clone $this->getSelect(); + + $this->joinCouponTable($couponCode, $couponSelect); + + $notExpired = $this->getConnection()->quoteInto( + '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', + $this->_date->date()->format('Y-m-d') ); - $autoGeneratedCouponCondition = [ - $connection->quoteInto( - "main_table.coupon_type = ?", - \Magento\SalesRule\Model\Rule::COUPON_TYPE_AUTO - ), - $connection->quoteInto( - "rule_coupons.type = ?", - \Magento\SalesRule\Api\Data\CouponInterface::TYPE_GENERATED - ), - ]; - - $orWhereConditions = [ - "(" . implode($autoGeneratedCouponCondition, " AND ") . ")", - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 1 AND rule_coupons.type = 1)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - $connection->quoteInto( - '(main_table.coupon_type = ? AND main_table.use_auto_generation = 0 AND rule_coupons.type = 0)', - \Magento\SalesRule\Model\Rule::COUPON_TYPE_SPECIFIC - ), - ]; - - $andWhereConditions = [ - $connection->quoteInto( - 'rule_coupons.code = ?', - $couponCode - ), - $connection->quoteInto( - '(rule_coupons.expiration_date IS NULL OR rule_coupons.expiration_date >= ?)', - $this->_date->date()->format('Y-m-d') - ), - ]; - - $orWhereCondition = implode(' OR ', $orWhereConditions); - $andWhereCondition = implode(' AND ', $andWhereConditions); - - $select->where( - '(' . $orWhereCondition . ') AND ' . $andWhereCondition, + $isAutogenerated = + $this->getConnection()->quoteInto('main_table.coupon_type = ?', Rule::COUPON_TYPE_AUTO) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.type = ?', CouponInterface::TYPE_GENERATED); + + $isValidSpecific = + $this->getConnection()->quoteInto('(main_table.coupon_type = ?)', Rule::COUPON_TYPE_SPECIFIC) + . ' AND (' . + '(main_table.use_auto_generation = 1 AND rule_coupons.type = 1)' + . ' OR ' . + '(main_table.use_auto_generation = 0 AND rule_coupons.type = 0)' + . ')'; + + $couponSelect->where( + "$notExpired AND ($isAutogenerated OR $isValidSpecific)", null, Select::TYPE_CONDITION ); - $select->group('main_table.rule_id'); - return $connection->fetchCol($select); + return $couponSelect; + } + + /** + * Join coupon table to select. + * + * @param string $couponCode + * @param Select $couponSelect + */ + private function joinCouponTable($couponCode, Select $couponSelect) + { + $couponJoinCondition = + 'main_table.rule_id = rule_coupons.rule_id' + . ' AND ' . + $this->getConnection()->quoteInto('main_table.coupon_type <> ?', Rule::COUPON_TYPE_NO_COUPON) + . ' AND ' . + $this->getConnection()->quoteInto('rule_coupons.code = ?', $couponCode); + + $couponSelect->joinInner( + ['rule_coupons' => $this->getTable('salesrule_coupon')], + $couponJoinCondition, + [Coupon::KEY_CODE] + ); } /** diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index fd5953697c7db..96867222d170a 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -58,6 +58,7 @@ public function __construct( public function loadAttributeOptions() { $attributes = [ + 'base_subtotal_with_discount' => __('Subtotal (Excl. Tax)'), 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 201df99aa5187..64e170580a449 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -184,6 +184,8 @@ protected function _getRules(Address $address = null) } /** + * Address id getter. + * * @param Address $address * @return string */ @@ -329,21 +331,7 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = $rule->getDiscountAmount(); break; case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION: - $cartRules = $address->getCartFixedRules(); - if (!isset($cartRules[$rule->getId()])) { - $cartRules[$rule->getId()] = $rule->getDiscountAmount(); - } - if ($cartRules[$rule->getId()] > 0) { - $quoteAmount = $this->priceCurrency->convert($cartRules[$rule->getId()], $quote->getStore()); - $discountAmount = min($shippingAmount - $address->getShippingDiscountAmount(), $quoteAmount); - $baseDiscountAmount = min( - $baseShippingAmount - $address->getBaseShippingDiscountAmount(), - $cartRules[$rule->getId()] - ); - $cartRules[$rule->getId()] -= $baseDiscountAmount; - } - - $address->setCartFixedRules($cartRules); + // Shouldn't be proceed according to MAGETWO-96403 break; } @@ -521,6 +509,8 @@ public function sortItemsByPriority($items, Address $address = null) } /** + * Rule total items getter. + * * @param int $key * @return array * @throws \Magento\Framework\Exception\LocalizedException @@ -535,6 +525,8 @@ public function getRuleItemTotalsInfo($key) } /** + * Decrease rule items count. + * * @param int $key * @return $this */ diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index c0589f3acfda6..e85eea62d473e 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -7,7 +7,7 @@ --> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/dataProfileSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="ApiSalesRule" type="SalesRule"> <data key="name" unique="suffix">salesRule</data> <data key="description">Sales Rule Descritpion</data> @@ -141,7 +141,6 @@ <data key="uses_per_coupon">0</data> <data key="simple_free_shipping">0</data> </entity> - <entity name="SalesRuleSpecificCouponWithFixedDiscount" type="SalesRule"> <data key="name" unique="suffix">SimpleSalesRule</data> <data key="description">Sales Rule Description</data> @@ -168,6 +167,36 @@ <data key="uses_per_coupon">10</data> <data key="simple_free_shipping">1</data> </entity> + <entity name="CartPriceRuleWithRewardPointsOnly" type="SalesRule"> + <data key="name" unique="suffix">SalesRuleReward</data> + <data key="description">Sales Rule with Reward Point</data> + <array key="website_ids"> + <item>1</item> + </array> + <array key="customer_group_ids"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </array> + <data key="uses_per_customer">10</data> + <data key="is_active">true</data> + <data key="stop_rules_processing">true</data> + <data key="is_advanced">true</data> + <data key="sort_order">0</data> + <data key="simple_action">by_percent</data> + <data key="discount_amount">0</data> + <data key="discount_qty">0</data> + <data key="discount_step">0</data> + <data key="apply_to_shipping">false</data> + <data key="times_used">0</data> + <data key="is_rss">true</data> + <data key="coupon_type">NO_COUPON</data> + <data key="use_auto_generation">false</data> + <data key="uses_per_coupon">10</data> + <data key="simple_free_shipping">0</data> + <requiredEntity type="sales-rule-extension-attribute">SalesRuleExtensionAttribute</requiredEntity> + </entity> <entity name="PriceRuleWithCondition" type="SalesRule"> <data key="name" unique="suffix">SalesRule</data> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleExtensionAttributeData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleExtensionAttributeData.xml new file mode 100644 index 0000000000000..43ff4d897c143 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleExtensionAttributeData.xml @@ -0,0 +1,13 @@ +<?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="SalesRuleExtensionAttribute" type="sales-rule-extension-attribute"> + <data key="reward_points_delta">200</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales-rule-extension-attribute-meta.xml b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales-rule-extension-attribute-meta.xml new file mode 100644 index 0000000000000..51c4ac24a7426 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales-rule-extension-attribute-meta.xml @@ -0,0 +1,12 @@ +<!-- + /** + * 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="SetSalesRuleExtensionAttribute" dataType="sales-rule-extension-attribute" type="create"> + <field key="reward_points_delta">integer</field> + </operation> +</operations> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule-meta.xml b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule-meta.xml index 0d4c4356a20a7..38009c510d2be 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule-meta.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Metadata/sales_rule-meta.xml @@ -6,7 +6,7 @@ */ --> <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="CreateSalesRule" dataType="SalesRule" type="create" auth="adminOauth" url="/V1/salesRules" method="POST"> <contentType>application/json</contentType> <object key="rule" dataType="SalesRule"> @@ -30,6 +30,7 @@ <field key="simple_free_shipping">string</field> <field key="stop_rules_processing">boolean</field> <field key="is_advanced">boolean</field> + <field key="extension_attributes">sales-rule-extension-attribute</field> <array key="store_labels"> <!-- specify object name as array value --> <value>SalesRuleStoreLabel</value> @@ -67,9 +68,6 @@ <field key="value">string</field> <field key="extension_attributes">empty_extension_attribute</field> </object> - <object dataType="ExtensionAttribute" key="extension_attributes"> - <field key="reward_points_delta">integer</field> - </object> </object> </operation> <operation name="DeleteSalesRule" dataType="SalesRule" type="delete" auth="adminOauth" url="/V1/salesRules/{rule_id}" method="DELETE"> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 42448565791c5..a4b45813a10bc 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -5,6 +5,13 @@ */ namespace Magento\SalesRule\Test\Unit\Model; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Model\Quote; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Validator; +use Magento\Store\Model\Store; +use PHPUnit_Framework_MockObject_MockObject as MockObject; + /** * Class ValidatorTest * @@SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -17,50 +24,55 @@ class ValidatorTest extends \PHPUnit\Framework\TestCase protected $helper; /** - * @var \Magento\SalesRule\Model\Validator + * @var Validator */ protected $model; /** - * @var \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Item|MockObject */ protected $item; /** - * @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Quote\Model\Quote\Address|MockObject */ protected $addressMock; /** - * @var \Magento\SalesRule\Model\RulesApplier|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\RulesApplier|MockObject */ protected $rulesApplier; /** - * @var \Magento\SalesRule\Model\Validator\Pool|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Validator\Pool|MockObject */ protected $validators; /** - * @var \Magento\SalesRule\Model\Utility|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\Utility|MockObject */ protected $utility; /** - * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection|MockObject */ protected $ruleCollection; /** - * @var \Magento\Catalog\Helper\Data|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Catalog\Helper\Data|MockObject */ protected $catalogData; /** - * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Magento\Framework\Message\ManagerInterface|MockObject */ protected $messageManager; + /** + * @var PriceCurrencyInterface|MockObject + */ + private $priceCurrency; + protected function setUp() { $this->helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); @@ -74,6 +86,7 @@ protected function setUp() ->setMethods( [ 'getShippingAmountForDiscount', + 'getBaseShippingAmountForDiscount', 'getQuote', 'getCustomAttributesCodes', 'setCartFixedRules' @@ -81,7 +94,7 @@ protected function setUp() ) ->getMock(); - /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */ + /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|MockObject $item */ $this->item = $this->createPartialMock( \Magento\Quote\Model\Quote\Item::class, ['__wakeup', 'getAddress', 'getParentItemId'] @@ -100,10 +113,13 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); $ruleCollectionFactoryMock = $this->prepareRuleCollectionMock($this->ruleCollection); + $this->priceCurrency = $this->getMockBuilder(PriceCurrencyInterface::class) + ->disableOriginalConstructor() + ->getMock(); - /** @var \Magento\SalesRule\Model\Validator|\PHPUnit_Framework_MockObject_MockObject $validator */ + /** @var Validator|MockObject $validator */ $this->model = $this->helper->getObject( - \Magento\SalesRule\Model\Validator::class, + Validator::class, [ 'context' => $context, 'registry' => $registry, @@ -112,7 +128,8 @@ protected function setUp() 'utility' => $this->utility, 'rulesApplier' => $this->rulesApplier, 'validators' => $this->validators, - 'messageManager' => $this->messageManager + 'messageManager' => $this->messageManager, + 'priceCurrency' => $this->priceCurrency ] ); $this->model->setWebsiteId(1); @@ -131,7 +148,7 @@ protected function setUp() } /** - * @return \Magento\Quote\Model\Quote\Item|\PHPUnit_Framework_MockObject_MockObject + * @return \Magento\Quote\Model\Quote\Item|MockObject */ protected function getQuoteItemMock() { @@ -145,8 +162,8 @@ protected function getQuoteItemMock() $itemSimple = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, ['getAddress', '__wakeup']); $itemSimple->expects($this->any())->method('getAddress')->will($this->returnValue($this->addressMock)); - /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getStoreId', '__wakeup']); + /** @var $quote Quote */ + $quote = $this->createPartialMock(Quote::class, ['getStoreId', '__wakeup']); $quote->expects($this->any())->method('getStoreId')->will($this->returnValue(1)); $itemData = include $fixturePath . 'quote_item_downloadable.php'; @@ -168,7 +185,7 @@ public function testCanApplyRules() $this->model->getCouponCode() ); $item = $this->getQuoteItemMock(); - $rule = $this->createMock(\Magento\SalesRule\Model\Rule::class); + $rule = $this->createMock(Rule::class); $actionsCollection = $this->createPartialMock(\Magento\Rule\Model\Action\Collection::class, ['validate']); $actionsCollection->expects($this->any()) ->method('validate') @@ -278,7 +295,7 @@ public function testApplyRulesThatAppliedRuleIdsAreCollected() public function testInit() { $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->init( $this->model->getWebsiteId(), $this->model->getCustomerGroupId(), @@ -314,7 +331,7 @@ public function testCanApplyDiscount() public function testInitTotalsCanApplyDiscount() { $rule = $this->createPartialMock( - \Magento\SalesRule\Model\Rule::class, + Rule::class, ['getSimpleAction', 'getActions', 'getId'] ); $item1 = $this->getMockForAbstractClass( @@ -337,7 +354,7 @@ public function testInitTotalsCanApplyDiscount() $rule->expects($this->any()) ->method('getSimpleAction') - ->willReturn(\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION); + ->willReturn(Rule::CART_FIXED_ACTION); $iterator = new \ArrayIterator([$rule]); $this->ruleCollection->expects($this->once())->method('getIterator')->willReturn($iterator); $validator = $this->getMockBuilder(\Magento\Framework\Validator\AbstractValidator::class) @@ -392,7 +409,7 @@ public function testInitTotalsNoItems() /** * @param $ruleCollection - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function prepareRuleCollectionMock($ruleCollection) { @@ -427,14 +444,14 @@ public function testProcessShippingAmountNoRules() $this->model->getCouponCode() ); $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->processShippingAmount($this->setupAddressMock()) ); } public function testProcessShippingAmountProcessDisabled() { - $ruleMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule::class) + $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); @@ -448,51 +465,54 @@ public function testProcessShippingAmountProcessDisabled() $this->model->getCouponCode() ); $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, + Validator::class, $this->model->processShippingAmount($this->setupAddressMock()) ); } /** + * Tests shipping amounts according to rule simple action. + * * @param string $action + * @param int $ruleDiscount + * @param int $shippingDiscount * @dataProvider dataProviderActions */ - public function testProcessShippingAmountActions($action) + public function testProcessShippingAmountActions($action, $ruleDiscount, $shippingDiscount) { - $discountAmount = 50; + $shippingAmount = 5; - $ruleMock = $this->getMockBuilder(\Magento\SalesRule\Model\Rule::class) + $ruleMock = $this->getMockBuilder(Rule::class) ->disableOriginalConstructor() ->setMethods(['getApplyToShipping', 'getSimpleAction', 'getDiscountAmount']) ->getMock(); - $ruleMock->expects($this->any()) - ->method('getApplyToShipping') + $ruleMock->method('getApplyToShipping') ->willReturn(true); - $ruleMock->expects($this->any()) - ->method('getDiscountAmount') - ->willReturn($discountAmount); - $ruleMock->expects($this->any()) - ->method('getSimpleAction') + $ruleMock->method('getDiscountAmount') + ->willReturn($ruleDiscount); + $ruleMock->method('getSimpleAction') ->willReturn($action); $iterator = new \ArrayIterator([$ruleMock]); - $this->ruleCollection->expects($this->any()) - ->method('getIterator') + $this->ruleCollection->method('getIterator') ->willReturn($iterator); - $this->utility->expects($this->any()) - ->method('canProcessRule') + $this->utility->method('canProcessRule') ->willReturn(true); + $this->priceCurrency->method('convert') + ->willReturn($ruleDiscount); + $this->model->init( $this->model->getWebsiteId(), $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->assertInstanceOf( - \Magento\SalesRule\Model\Validator::class, - $this->model->processShippingAmount($this->setupAddressMock(5)) - ); + + $addressMock = $this->setupAddressMock($shippingAmount); + + self::assertInstanceOf(Validator::class, $this->model->processShippingAmount($addressMock)); + self::assertEquals($shippingDiscount, $addressMock->getShippingDiscountAmount()); } /** @@ -501,44 +521,48 @@ public function testProcessShippingAmountActions($action) public static function dataProviderActions() { return [ - [\Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION], - [\Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION], - [\Magento\SalesRule\Model\Rule::TO_FIXED_ACTION], - [\Magento\SalesRule\Model\Rule::BY_FIXED_ACTION], - [\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION], + [Rule::TO_PERCENT_ACTION, 50, 2.5], + [Rule::BY_PERCENT_ACTION, 50, 2.5], + [Rule::TO_FIXED_ACTION, 5, 0], + [Rule::BY_FIXED_ACTION, 5, 5], + [Rule::CART_FIXED_ACTION, 5, 0], ]; } /** * @param null|int $shippingAmount - * @return \PHPUnit_Framework_MockObject_MockObject + * @return MockObject */ protected function setupAddressMock($shippingAmount = null) { - $storeMock = $this->getMockBuilder(\Magento\Store\Model\Store::class) + $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->setMethods(['setAppliedRuleIds', 'getStore']) ->getMock(); - $quoteMock->expects($this->any()) - ->method('getStore') + + $quoteMock->method('getStore') ->willReturn($storeMock); - $quoteMock->expects($this->any()) - ->method('setAppliedRuleIds') + + $quoteMock->method('setAppliedRuleIds') ->willReturnSelf(); - $this->addressMock->expects($this->any()) - ->method('getShippingAmountForDiscount') + $this->addressMock->method('getShippingAmountForDiscount') ->willReturn($shippingAmount); - $this->addressMock->expects($this->any()) - ->method('getQuote') + + $this->addressMock->method('getBaseShippingAmountForDiscount') + ->willReturn($shippingAmount); + + $this->addressMock->method('getQuote') ->willReturn($quoteMock); - $this->addressMock->expects($this->any()) - ->method('getCustomAttributesCodes') + + $this->addressMock->method('getCustomAttributesCodes') ->willReturn([]); + return $this->addressMock; } @@ -546,7 +570,7 @@ public function testReset() { $this->utility->expects($this->once()) ->method('resetRoundingDeltas'); - $quoteMock = $this->getMockBuilder(\Magento\Quote\Model\Quote::class) + $quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->getMock(); $addressMock = $this->getMockBuilder(\Magento\Quote\Model\Quote\Address::class) @@ -560,6 +584,6 @@ public function testReset() $this->model->getCustomerGroupId(), $this->model->getCouponCode() ); - $this->assertInstanceOf(\Magento\SalesRule\Model\Validator::class, $this->model->reset($addressMock)); + $this->assertInstanceOf(Validator::class, $this->model->reset($addressMock)); } } diff --git a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml index 9b579f47759a6..570eb0bf151f0 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml +++ b/app/code/Magento/SalesRule/view/adminhtml/ui_component/sales_rule_form.xml @@ -452,7 +452,7 @@ <dataScope>discount_step</dataScope> </settings> </field> - <field name="apply_to_shipping" component="Magento_Ui/js/form/element/single-checkbox-toggle-notice" formElement="checkbox"> + <field name="apply_to_shipping" component="Magento_SalesRule/js/form/element/apply_to_shipping" formElement="checkbox"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">sales_rule</item> diff --git a/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js new file mode 100644 index 0000000000000..dfb3f909345b3 --- /dev/null +++ b/app/code/Magento/SalesRule/view/adminhtml/web/js/form/element/apply_to_shipping.js @@ -0,0 +1,37 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/single-checkbox-toggle-notice' +], function (Checkbox) { + 'use strict'; + + return Checkbox.extend({ + defaults: { + imports: { + toggleDisabled: '${ $.parentName }.simple_action:value' + } + }, + + /** + * Toggle element disabled state according to simple action value. + * + * @param {String} action + */ + toggleDisabled: function (action) { + switch (action) { + case 'cart_fixed': + this.disabled(true); + break; + default: + this.disabled(false); + } + + if (this.disabled()) { + this.checked(false); + } + } + }); +}); diff --git a/app/code/Magento/Security/Model/SecurityChecker/Quantity.php b/app/code/Magento/Security/Model/SecurityChecker/Quantity.php index 7f1d85abeb74b..cbf41980c51d3 100644 --- a/app/code/Magento/Security/Model/SecurityChecker/Quantity.php +++ b/app/code/Magento/Security/Model/SecurityChecker/Quantity.php @@ -47,13 +47,13 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function check($securityEventType, $accountReference = null, $longIp = null) { $isEnabled = $this->securityConfig->getPasswordResetProtectionType() != ResetMethod::OPTION_NONE; $allowedAttemptsNumber = $this->securityConfig->getMaxNumberPasswordResetRequests(); - if ($isEnabled and $allowedAttemptsNumber) { + if ($isEnabled && $allowedAttemptsNumber) { $collection = $this->prepareCollection($securityEventType, $accountReference, $longIp); if ($collection->count() >= $allowedAttemptsNumber) { throw new SecurityViolationException( diff --git a/app/code/Magento/SendFriend/Model/SendFriend.php b/app/code/Magento/SendFriend/Model/SendFriend.php index c69d6342b4892..38525a9f83a12 100644 --- a/app/code/Magento/SendFriend/Model/SendFriend.php +++ b/app/code/Magento/SendFriend/Model/SendFriend.php @@ -16,6 +16,7 @@ * @method \Magento\SendFriend\Model\SendFriend setTime(int $value) * * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api @@ -162,6 +163,8 @@ protected function _construct() } /** + * Send email. + * * @return $this * @throws CoreException */ @@ -236,7 +239,7 @@ public function validate() } $email = $this->getSender()->getEmail(); - if (empty($email) or !\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { + if (empty($email) || !\Zend_Validate::is($email, \Magento\Framework\Validator\EmailAddress::class)) { $errors[] = __('Invalid Sender Email'); } @@ -281,13 +284,13 @@ public function setRecipients($recipients) // validate array if (!is_array( $recipients - ) or !isset( + ) || !isset( $recipients['email'] - ) or !isset( + ) || !isset( $recipients['name'] - ) or !is_array( + ) || !is_array( $recipients['email'] - ) or !is_array( + ) || !is_array( $recipients['name'] ) ) { @@ -487,7 +490,7 @@ protected function _sentCountByCookies($increment = false) $oldTimes = explode(',', $oldTimes); foreach ($oldTimes as $oldTime) { $periodTime = $time - $this->_sendfriendData->getPeriod(); - if (is_numeric($oldTime) and $oldTime >= $periodTime) { + if (is_numeric($oldTime) && $oldTime >= $periodTime) { $newTimes[] = $oldTime; } } diff --git a/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php b/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php new file mode 100644 index 0000000000000..661068d42c35d --- /dev/null +++ b/app/code/Magento/Shipping/Block/DataProviders/Tracking/DeliveryDateTitle.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Shipping\Block\DataProviders\Tracking; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Shipping\Model\Tracking\Result\Status; + +/** + * Extension point to provide ability to change tracking details titles + */ +class DeliveryDateTitle implements ArgumentInterface +{ + /** + * Return title if carrier is defined + * + * @param Status $trackingStatus + * @return \Magento\Framework\Phrase|string + */ + public function getTitle(Status $trackingStatus) + { + return $trackingStatus->getCarrier() ? __('Delivered on:') : ''; + } +} diff --git a/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml b/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml index 1f5b0ae4630ad..67d03da2599bf 100644 --- a/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml +++ b/app/code/Magento/Shipping/view/frontend/layout/shipping_tracking_popup.xml @@ -8,7 +8,11 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="empty" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="content"> - <block class="Magento\Shipping\Block\Tracking\Popup" name="shipping.tracking.popup" template="Magento_Shipping::tracking/popup.phtml" cacheable="false" /> + <block class="Magento\Shipping\Block\Tracking\Popup" name="shipping.tracking.popup" template="Magento_Shipping::tracking/popup.phtml" cacheable="false"> + <arguments> + <argument name="delivery_date_title" xsi:type="object">Magento\Shipping\Block\DataProviders\Tracking\DeliveryDateTitle</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml index 9253b47f82f5d..e8584d8f6ad51 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/details.phtml @@ -77,7 +77,7 @@ $number = is_object($track) ? $track->getTracking() : $track['number']; <?php if ($track->getDeliverydate()): ?> <tr> - <th class="col label" scope="row"><?= $block->escapeHtml(__('Delivered on:')) ?></th> + <th class="col label" scope="row"><?= $block->escapeHtml($parentBlock->getDeliveryDateTitle()->getTitle($track)) ?></th> <td class="col value"> <?= /* @noEscape */ $parentBlock->formatDeliveryDateTime($track->getDeliverydate(), $track->getDeliverytime()) ?> </td> diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index dda1697da7fdf..7279f6eda85d7 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -1,4 +1,5 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -155,12 +156,11 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento protected $dateTime; /** - * Model cache tag for clear cache in after save and after delete + * @inheritdoc * - * @var string * @since 100.2.0 */ - protected $_cacheTag = true; + protected $_cacheTag = [Value::CACHE_TAG]; /** * Last mode min timestamp value diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml index f1c6f4d87e0d6..2f541feacd9c2 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewActionGroup.xml @@ -24,8 +24,8 @@ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReolad"/> - <see userInput="You saved the store view." stepKey="seeSavedMessage" /> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForPageReolad"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the store view." stepKey="seeSavedMessage" /> </actionGroup> <actionGroup name="AdminCreateStoreViewUseStringArgumentsActionGroup" extends="AdminCreateStoreViewActionGroup"> <arguments> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml index d540a0000655f..1c56c3cc44220 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSwitchStoreViewActionGroup.xml @@ -16,6 +16,7 @@ <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitingForInformationModal"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmScopeSwitch"/> <waitForPageLoad stepKey="waitForScopeSwitched"/> + <scrollToTopOfPage stepKey="scrollToStoreSwitcher"/> <see userInput="{{scopeName}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewScopeName"/> </actionGroup> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml index e6ebd229e4683..c62ed55c7df49 100644 --- a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreViewActionGroup.xml @@ -13,7 +13,7 @@ </arguments> <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher"/> <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown"/> - <click selector="{{StorefrontHeaderSection.storeViewOption(storeView.name)}}" stepKey="clickSelectStoreView"/> + <click selector="{{StorefrontHeaderSection.storeViewOption(storeView.code)}}" stepKey="clickSelectStoreView"/> <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php index 85633475205ed..3df47ee749587 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php @@ -8,6 +8,8 @@ use Magento\Catalog\Block\Product\Context; use Magento\Catalog\Helper\Product as CatalogProduct; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Layer\Category as CategoryLayer; use Magento\ConfigurableProduct\Helper\Data; use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; use Magento\Customer\Helper\Session\CurrentCustomer; @@ -39,6 +41,12 @@ class Configurable extends \Magento\Swatches\Block\Product\Renderer\Configurable private $variationPrices; /** + * @var \Magento\Catalog\Model\Layer\Resolver + */ + private $layerResolver; + + /** + * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @param Context $context * @param ArrayUtils $arrayUtils * @param EncoderInterface $jsonEncoder @@ -49,11 +57,11 @@ class Configurable extends \Magento\Swatches\Block\Product\Renderer\Configurable * @param ConfigurableAttributeData $configurableAttributeData * @param SwatchData $swatchHelper * @param Media $swatchMediaHelper - * @param array $data other data - * @param SwatchAttributesProvider $swatchAttributesProvider + * @param array $data + * @param SwatchAttributesProvider|null $swatchAttributesProvider * @param \Magento\Framework\Locale\Format|null $localeFormat * @param \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices|null $variationPrices - * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @param Resolver $layerResolver */ public function __construct( Context $context, @@ -69,7 +77,8 @@ public function __construct( array $data = [], SwatchAttributesProvider $swatchAttributesProvider = null, \Magento\Framework\Locale\Format $localeFormat = null, - \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null, + Resolver $layerResolver = null ) { parent::__construct( $context, @@ -91,10 +100,11 @@ public function __construct( $this->variationPrices = $variationPrices ?: ObjectManager::getInstance()->get( \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class ); + $this->layerResolver = $layerResolver ?: ObjectManager::getInstance()->get(Resolver::class); } /** - * @return string + * @inheritdoc */ protected function getRendererTemplate() { @@ -120,7 +130,7 @@ protected function _toHtml() } /** - * @return array + * @inheritdoc */ protected function getSwatchAttributesData() { @@ -182,6 +192,7 @@ protected function getOptionImages() * Add images to result json config in case of Layered Navigation is used * * @return array + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) * @since 100.2.0 */ protected function _getAdditionalConfig() @@ -246,4 +257,17 @@ private function getLayeredAttributesIfExists(Product $configurableProduct, arra return $layeredAttributes; } + + /** + * @inheritdoc + */ + public function getCacheKeyInfo() + { + $cacheKeyInfo = parent::getCacheKeyInfo(); + /** @var CategoryLayer $catalogLayer */ + $catalogLayer = $this->layerResolver->get(); + $cacheKeyInfo[] = $catalogLayer->getStateKey(); + + return $cacheKeyInfo; + } } diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index ae35f5203dd73..2c00224083bfc 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -249,18 +249,15 @@ public function loadVariationByFallback(Product $parentProduct, array $attribute $this->addFilterByParent($productCollection, $parentId); $configurableAttributes = $this->getAttributesFromConfigurable($parentProduct); - $allAttributesArray = []; + + $resultAttributesToFilter = []; foreach ($configurableAttributes as $attribute) { - if (!empty($attribute['default_value'])) { - $allAttributesArray[$attribute['attribute_code']] = $attribute['default_value']; + $attributeCode = $attribute->getData('attribute_code'); + if (array_key_exists($attributeCode, $attributes)) { + $resultAttributesToFilter[$attributeCode] = $attributes[$attributeCode]; } } - $resultAttributesToFilter = array_merge( - $attributes, - array_diff_key($allAttributesArray, $attributes) - ); - $this->addFilterByAttributes($productCollection, $resultAttributesToFilter); $variationProduct = $productCollection->getFirstItem(); diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurationsWithVisualSwatchActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurationsWithVisualSwatchActionGroup.xml index 4eb4ca0b1327b..4d6896086f91b 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurationsWithVisualSwatchActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/CreateConfigurationsWithVisualSwatchActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="CreateConfigurationsWithVisualSwatch"> <arguments> <argument name="attribute" defaultValue="VisualSwatchAttribute"/> @@ -59,4 +59,13 @@ <click selector="{{AdminChooseAffectedAttributeSetSection.confirm}}" stepKey="clickOnConfirmInPopup"/> <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> </actionGroup> + <actionGroup name="CreateConfigurationsWithVisualSwatchVisibleOnStorefront" extends="CreateConfigurationsWithVisualSwatch"> + <click selector="{{AdminNewAttributePanelSection.storefrontPropertiesTab}}" after="fillDefaultStoreLabel2" stepKey="expandStorefrontPropertiesTab"/> + <selectOption selector="{{AdminNewAttributePanelSection.useInSearch}}" userInput="1" after="expandStorefrontPropertiesTab" stepKey="enableUseInSearch"/> + <selectOption selector="{{AdminNewAttributePanelSection.visibleInAdvancedSearch}}" userInput="1" after="enableUseInSearch" stepKey="enableVisibleInAdvancedSearch"/> + <selectOption selector="{{AdminNewAttributePanelSection.comparableOnStorefront}}" userInput="1" after="enableVisibleInAdvancedSearch" stepKey="enableComparableOnStorefront"/> + <selectOption selector="{{AdminNewAttributePanelSection.useInLayeredNavigation}}" userInput="1" after="enableComparableOnStorefront" stepKey="enableUseInLayeredNavigation"/> + <selectOption selector="{{AdminNewAttributePanelSection.visibleOnCatalogPagesOnStorefront}}" userInput="1" after="enableUseInLayeredNavigation" stepKey="enableVisibleOnCatalogPagesOnStorefront"/> + <selectOption selector="{{AdminNewAttributePanelSection.useInProductListing}}" userInput="1" after="enableVisibleOnCatalogPagesOnStorefront" stepKey="enableUseInProductListing"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml new file mode 100644 index 0000000000000..f5bb9372266e0 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml @@ -0,0 +1,75 @@ +<?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="StorefrontSwatchAttributesDisplayInWidgetCMSTest"> + <annotations> + <features value="Swatches"/> + <stories value="Create/configure swatches product attribute"/> + <title value="Swatch Attribute is not displayed in the Widget CMS"/> + <description value="Swatch Attribute is not displayed in the Widget CMS"/> + <severity value="MAJOR"/> + <testCaseId value="MC-14844"/> + <useCaseId value="MAGETWO-94147"/> + <group value="configurableProduct"/> + <group value="swatches"/> + <group value="cms"/> + <group value="WYSIWYGDisabled"/> + </annotations> + + <before> + <!--create category--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <!--Login--> + <actionGroup ref="LoginAsAdmin" stepKey="loginToAdmin"/> + <!--Create a configurable swatch product via the UI --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="searchAndSelectCategory"/> + <!--Add swatch attribute to configurable product--> + <actionGroup ref="CreateConfigurationsWithVisualSwatchVisibleOnStorefront" stepKey="addSwatchToProduct"/> + <!--Create CMS page--> + <actionGroup ref="CreateNewPageWithWidgetWithCategoryCondition" stepKey="createCMSPageWithWidget"> + <argument name="categoryId" value="$$createCategory.id$$"/> + <argument name="conditionOperator" value="contains"/> + </actionGroup> + </before> + + <after> + <!--delete created category--> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <!--delete created configurable product--> + <actionGroup ref="DeleteAllProductsOnProductsGridPageFilteredByName" stepKey="deleteAllCreatedProducts"> + <argument name="product" value="BaseConfigurableProduct"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter"/> + <!--delete created attribute product--> + <actionGroup ref="DeleteProductAttribute" stepKey="deleteAttribute"> + <argument name="productAttribute" value="VisualSwatchAttribute"/> + </actionGroup> + <actionGroup ref="clearFiltersAdminDataGrid" stepKey="clearGridFilter1"/> + <!--delete created page product--> + <actionGroup ref="DeletePageByUrlKeyActionGroup" stepKey="deletePage"> + <argument name="urlKey" value="{{_defaultCmsPage.identifier}}"/> + </actionGroup> + <!--logout--> + <actionGroup ref="logout" stepKey="logoutFromAdmin"/> + </after> + <!--Open Storefront page for the new created page--> + <amOnPage url="{{StorefrontCmsPage.url(_defaultCmsPage.identifier)}}" stepKey="goToCreatedCmsPage"/> + <waitForElementVisible selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel(VisualSwatchOption1.default_label)}}" stepKey="waitForSwatchOptionsAppear"/> + <seeElement selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel(VisualSwatchOption1.default_label)}}" stepKey="assertAddedWidgetS"/> + <seeElement selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel(VisualSwatchOption2.default_label)}}" stepKey="assertAddedWidgetM"/> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php index 6b72422d05014..a876a4dcc85c1 100644 --- a/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Block/Product/Renderer/Listing/ConfigurableTest.php @@ -61,6 +61,21 @@ class ConfigurableTest extends \PHPUnit\Framework\TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ private $variationPricesMock; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $layerResolverMock; + + /** @var ObjectManagerHelper */ + private $objectManagerHelper; + + /** @var \Magento\Store\Model\StoreManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ + private $storeManagerMock; + + /** @var \Magento\Customer\Model\Session|\PHPUnit_Framework_MockObject_MockObject */ + private $customerSessionMock; + + /** + * @inheritdoc + */ public function setUp() { $this->arrayUtils = $this->createMock(\Magento\Framework\Stdlib\ArrayUtils::class); @@ -82,9 +97,35 @@ public function setUp() $this->variationPricesMock = $this->createMock( \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class ); + $this->layerResolverMock = $this->createMock(\Magento\Catalog\Model\Layer\Resolver::class); + + $this->storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + ->setMethods(['getStore']) + ->getMockForAbstractClass(); + $this->customerSessionMock = $this->getMockBuilder(\Magento\Customer\Model\Session::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerGroupId']) + ->getMock(); + + $this->objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->prepareObjectManager([ + [ + \Magento\Framework\Locale\Format::class, + $this->createMock(\Magento\Framework\Locale\Format::class), + ], + [ + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class, + $this->createMock( + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices::class + ), + ], + [ + \Magento\Customer\Model\Session::class, + $this->customerSessionMock, + ], + ]); - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->configurable = $objectManagerHelper->getObject( + $this->configurable = $this->objectManagerHelper->getObject( \Magento\Swatches\Block\Product\Renderer\Listing\Configurable::class, [ 'scopeConfig' => $this->scopeConfig, @@ -100,9 +141,15 @@ public function setUp() 'priceCurrency' => $this->priceCurrency, 'configurableAttributeData' => $this->configurableAttributeData, 'data' => [], - 'variationPrices' => $this->variationPricesMock + 'variationPrices' => $this->variationPricesMock, + 'layerResolver' => $this->layerResolverMock, ] ); + $this->objectManagerHelper->setBackwardCompatibleProperty( + $this->configurable, + '_storeManager', + $this->storeManagerMock + ); } /** @@ -235,4 +282,54 @@ public function testGetPricesJson() $this->jsonEncoder->expects($this->once())->method('encode')->with($expectedPrices); $this->configurable->getPricesJson(); } + + /** + * @param $map + */ + private function prepareObjectManager($map) + { + $objectManagerMock = $this->getMockBuilder(\Magento\Framework\ObjectManagerInterface::class) + ->setMethods(['getInstance']) + ->getMockForAbstractClass(); + $objectManagerMock->expects($this->any())->method('getInstance')->willReturnSelf(); + $objectManagerMock->expects($this->any()) + ->method('get') + ->will($this->returnValueMap($map)); + $reflectionClass = new \ReflectionClass(\Magento\Framework\App\ObjectManager::class); + $reflectionProperty = $reflectionClass->getProperty('_instance'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($objectManagerMock); + } + + /** + * Test receiving cache key info. + */ + public function testGetCacheKeyInfo() + { + $expected = [ + 0 => 'BLOCK_TPL', + 1 => 'default', + 2 => null, + 'base_url' => null, + 'template' => null, + 3 => 'USD', + 4 => null, + 5 => 'STORE_1_CAT_25_CUSTGROUP_0_color_53', + ]; + $stateKey = 'STORE_1_CAT_25_CUSTGROUP_0_color_53'; + + $storeMock = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)->getMockForAbstractClass(); + $storeMock->expects($this->any())->method('getCode')->willReturn('default'); + $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); + $currency = $this->getMockBuilder(\Magento\Directory\Model\Currency::class) + ->disableOriginalConstructor() + ->getMock(); + $this->priceCurrency->expects($this->once())->method('getCurrency')->willReturn($currency); + $currency->expects($this->once())->method('getCode')->willReturn('USD'); + $layer = $this->createMock(\Magento\Catalog\Model\Layer::class); + $this->layerResolverMock->expects($this->once())->method('get')->willReturn($layer); + $layer->expects($this->once())->method('getStateKey')->willReturn($stateKey); + + $this->assertEquals($this->configurable->getCacheKeyInfo(), $expected); + } } diff --git a/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php b/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php index 5732018f7615f..58f9ea821f0b4 100644 --- a/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php @@ -278,6 +278,8 @@ public function testLoadVariationByFallback($product) $metadataMock = $this->createMock(\Magento\Framework\EntityManager\EntityMetadataInterface::class); $this->metaDataPoolMock->expects($this->once())->method('getMetadata')->willReturn($metadataMock); $metadataMock->expects($this->once())->method('getLinkField')->willReturn('id'); + $this->productMock->expects($this->once())->method('getTypeInstance')->willReturn($this->configurableMock); + $this->attributeMock->method('getData')->with('attribute_code')->willReturn('color'); $this->getSwatchAttributes($product); diff --git a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css index 02a3f0324d955..67a5537e3274f 100644 --- a/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/adminhtml/web/css/swatches.css @@ -150,7 +150,19 @@ } .col-swatch-min-width { - min-width: 30px; + min-width: 65px; +} + +[class^=swatch-col], +[class^=col-]:not(.col-draggable):not(.col-default) { + min-width: 150px; +} + +#swatch-visual-options-panel, +#swatch-text-options-panel, +#manage-options-panel { + overflow: auto; + width: 100%; } .swatches-visual-col.unavailable:after { diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml new file mode 100644 index 0000000000000..fce81408cd14d --- /dev/null +++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="category.product.type.widget.details.renderers"> + <block class="Magento\Swatches\Block\Product\Renderer\Listing\Configurable" name="category.product.type.details.renderers.configurable" as="configurable" template="Magento_Swatches::product/listing/renderer.phtml" ifconfig="catalog/frontend/show_swatches_in_product_list"/> + </referenceBlock> + </body> +</page> diff --git a/app/code/Magento/Swatches/view/frontend/layout/cms_page_view.xml b/app/code/Magento/Swatches/view/frontend/layout/cms_page_view.xml new file mode 100644 index 0000000000000..2ef1e2fb2e803 --- /dev/null +++ b/app/code/Magento/Swatches/view/frontend/layout/cms_page_view.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <head> + <css src="Magento_Swatches::css/swatches.css"/> + </head> +</page> diff --git a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css index 67b9fffcf1282..fb1dcff2ecfb0 100644 --- a/app/code/Magento/Swatches/view/frontend/web/css/swatches.css +++ b/app/code/Magento/Swatches/view/frontend/web/css/swatches.css @@ -31,6 +31,10 @@ margin-top: 10px; } +.swatch-attribute-options:focus { + box-shadow: none; +} + .swatch-option { /*width: 30px;*/ padding: 1px 2px; @@ -47,6 +51,10 @@ text-overflow: ellipsis; } +.swatch-option:focus { + box-shadow: 0 0 3px 1px #68a8e0; +} + .swatch-option.text { background: #F0F0F0; color: #686868; diff --git a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js index 3e28982ad44d3..2cb8bd1c25d46 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/swatch-renderer.js @@ -918,7 +918,8 @@ define([ $productPrice = $product.find(this.options.selectorProductPrice), options = _.object(_.keys($widget.optionsMap), {}), result, - tierPriceHtml; + tierPriceHtml, + isShow; $widget.element.find('.' + $widget.options.classes.attributeClass + '[option-selected]').each(function () { var attributeId = $(this).attr('attribute-id'); @@ -935,11 +936,9 @@ define([ } ); - if (typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount) { - $(this.options.slyOldPriceSelector).show(); - } else { - $(this.options.slyOldPriceSelector).hide(); - } + isShow = typeof result != 'undefined' && result.oldPrice.amount !== result.finalPrice.amount; + + $product.find(this.options.slyOldPriceSelector)[isShow ? 'show' : 'hide'](); if (typeof result != 'undefined' && result.tierPrices.length) { if (this.options.tierPriceTemplate) { diff --git a/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php b/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php index 82e2048283a9b..865b656918cc1 100755 --- a/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php +++ b/app/code/Magento/Tax/Model/Sales/Total/Quote/Tax.php @@ -264,7 +264,7 @@ protected function processExtraTaxables(Address\Total $total, array $itemsByType { $extraTaxableDetails = []; foreach ($itemsByType as $itemType => $itemTaxDetails) { - if ($itemType != self::ITEM_TYPE_PRODUCT and $itemType != self::ITEM_TYPE_SHIPPING) { + if ($itemType != self::ITEM_TYPE_PRODUCT && $itemType != self::ITEM_TYPE_SHIPPING) { foreach ($itemTaxDetails as $itemCode => $itemTaxDetail) { /** @var \Magento\Tax\Api\Data\TaxDetailsInterface $taxDetails */ $taxDetails = $itemTaxDetail[self::KEY_ITEM]; @@ -407,6 +407,7 @@ protected function enhanceTotalData( /** * Process model configuration array. + * * This method can be used for changing totals collect sort order * * @param array $config diff --git a/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php b/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php index 87f65ef311ac2..208833733ae3f 100644 --- a/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php +++ b/app/code/Magento/Tax/Plugin/Checkout/CustomerData/Cart.php @@ -6,6 +6,10 @@ namespace Magento\Tax\Plugin\Checkout\CustomerData; +/** + * Process quote items price, considering tax configuration. + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ class Cart { /** @@ -68,6 +72,16 @@ public function afterGetSectionData(\Magento\Checkout\CustomerData\Cart $subject $this->itemPriceRenderer->setItem($item); $this->itemPriceRenderer->setTemplate('checkout/cart/item/price/sidebar.phtml'); $result['items'][$key]['product_price']=$this->itemPriceRenderer->toHtml(); + if ($this->itemPriceRenderer->displayPriceExclTax()) { + $result['items'][$key]['product_price_value'] = $item->getCalculationPrice(); + } elseif ($this->itemPriceRenderer->displayPriceInclTax()) { + $result['items'][$key]['product_price_value'] = $item->getPriceInclTax(); + } elseif ($this->itemPriceRenderer->displayBothPrices()) { + //unset product price value in case price already has been set as scalar value. + unset($result['items'][$key]['product_price_value']); + $result['items'][$key]['product_price_value']['incl_tax'] = $item->getPriceInclTax(); + $result['items'][$key]['product_price_value']['excl_tax'] = $item->getCalculationPrice(); + } } } } diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml index 28ee9efc9f0a2..cbe5d9e158bb3 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml @@ -21,5 +21,6 @@ <element name="rowActionSelect" type="button" selector="[data-role='grid'] tbody tr .action-select-wrap"/> <element name="rowEditAction" type="button" selector="[data-role='grid'] tbody tr .action-select-wrap._active [data-action='item-edit']" timeout="30"/> <element name="dataGridEmpty" type="block" selector=".data-grid-tr-no-data td"/> + <element name="dataGridWrap" type="block" selector=".admin__data-grid-wrap"/> </section> </sections> diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/country.js b/app/code/Magento/Ui/view/base/web/js/form/element/country.js index f64a80bf535ec..c75301018e190 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/country.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/country.js @@ -49,7 +49,7 @@ define([ if (!this.value()) { defaultCountry = _.filter(result, function (item) { - return item['is_default'] && item['is_default'].includes(value); + return item['is_default'] && _.contains(item['is_default'], value); }); if (defaultCountry.length) { diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 4b178622c28cf..1fbde3601cc18 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -14,7 +14,8 @@ define([ 'Magento_Ui/js/lib/validation/validator', 'Magento_Ui/js/form/element/abstract', 'mage/translate', - 'jquery/file-uploader' + 'jquery/file-uploader', + 'mage/adminhtml/tools' ], function ($, _, utils, uiAlert, validator, Element, $t) { 'use strict'; 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 fa445a2577adb..be825a391cf07 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 @@ -18,7 +18,7 @@ define([ return Element.extend({ defaults: { template: 'ui/grid/search/search', - placeholder: $t('Search by keyword'), + placeholder: 'Search by keyword', label: $t('Keyword'), value: '', previews: [], diff --git a/app/code/Magento/Ui/view/base/web/templates/form/field.html b/app/code/Magento/Ui/view/base/web/templates/form/field.html index ed84e158819a2..6a095b4da14ed 100644 --- a/app/code/Magento/Ui/view/base/web/templates/form/field.html +++ b/app/code/Magento/Ui/view/base/web/templates/form/field.html @@ -8,8 +8,8 @@ visible="visible" css="$data.additionalClasses" attr="'data-index': index"> - <div class="admin__field-label"> - <label if="$data.label" visible="$data.labelVisible" attr="for: uid"> + <div class="admin__field-label" visible="$data.labelVisible"> + <label if="$data.label" attr="for: uid"> <span translate="label" attr="'data-config-scope': $data.scopeLabel" /> </label> </div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html index 3ef64fd4b5371..36a3232c3e61a 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/bookmarks.html @@ -6,7 +6,7 @@ --> <div class="admin__action-dropdown-wrap admin__data-grid-action-bookmarks" collapsible> <button class="admin__action-dropdown" type="button" toggleCollapsible> - <span class="admin__action-dropdown-text" text="activeView.label"/> + <span class="admin__action-dropdown-text" translate="activeView.label"/> </button> <ul class="admin__action-dropdown-menu"> <repeat args="foreach: viewsArray, item: '$view'"> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html index b52669e2cd28d..521ce9fc806ac 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/controls/bookmarks/view.html @@ -30,7 +30,7 @@ </div> <div class="action-dropdown-menu-item"> - <a href="" class="action-dropdown-menu-link" text="$view().label" click="applyView.bind($data, $view().index)" closeCollapsible/> + <a href="" class="action-dropdown-menu-link" translate="$view().label" click="applyView.bind($data, $view().index)" closeCollapsible/> <div class="action-dropdown-menu-item-actions" if="$view().editable"> <button class="action-edit" type="button" attr="title: $t('Edit bookmark')" click="editView.bind($data, $view().index)"> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html index 56244422a6b43..1ad0e7505ec9d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select-optgroup.html @@ -19,7 +19,7 @@ css: { _selected: $parent.root.isSelected(option.value), _hover: $parent.root.isHovered(option, $element), - _expended: $parent.root.getLevelVisibility($data), + _expended: $parent.root.getLevelVisibility($data) || $data.visible, _unclickable: $parent.root.isLabelDecoration($data), _last: $parent.root.addLastElement($data), '_with-checkbox': $parent.root.showCheckbox diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html index a96a4163caf7e..0411063c11512 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html @@ -125,12 +125,13 @@ css: { _selected: $parent.isSelected(option.value), _hover: $parent.isHovered(option, $element), - _expended: $parent.getLevelVisibility($data), + _expended: $parent.getLevelVisibility($data) && $parent.showLevels($data), _unclickable: $parent.isLabelDecoration($data), _last: $parent.addLastElement($data), '_with-checkbox': $parent.showCheckbox }, click: function(data, event){ + $parent.showLevels($data); $parent.toggleOptionSelected($data, $index(), event); }, clickBubble: false diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html index 39d996e05c3a6..13b82a93eca25 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/search/search.html @@ -10,9 +10,10 @@ </label> <input class="admin__control-text data-grid-search-control" type="text" data-bind=" + i18n: placeholder, attr: { id: index, - placeholder: placeholder + placeholder: $t(placeholder) }, textInput: inputValue, keyboard: { diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html index c5d87a4b16c4e..610d78e00b81d 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/submenu.html @@ -6,7 +6,7 @@ --> <ul class="action-submenu" each="data: action.actions, as: 'action'" css="_active: action.visible"> <li css="_visible: $data.visible"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html index 1aeb48b7c7698..d11d4aa243737 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/tree-massactions.html @@ -11,7 +11,7 @@ <div class="action-menu-items"> <ul class="action-menu" each="data: actions, as: 'action'" css="_active: opened"> <li css="_visible: $data.visible, _parent: $data.actions"> - <span class="action-menu-item" text="label" click="$parent.applyAction.bind($parent, type)"/> + <span class="action-menu-item" translate="label" click="$parent.applyAction.bind($parent, type)"/> <render args="name: $parent.submenuTemplate, data: $parent" if="$data.actions"/> </li> </ul> diff --git a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php index 60b845f95e5cc..f98801c266109 100644 --- a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php +++ b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php @@ -14,6 +14,9 @@ use Psr\Log\LoggerInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite as UrlRewriteData; +/** + * DB storage implementation for url rewrites + */ class DbStorage extends AbstractStorage { /** @@ -37,7 +40,7 @@ class DbStorage extends AbstractStorage protected $resource; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ private $logger; @@ -45,7 +48,7 @@ class DbStorage extends AbstractStorage * @param \Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory $urlRewriteFactory * @param DataObjectHelper $dataObjectHelper * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Psr\Log\LoggerInterface|null $logger + * @param LoggerInterface|null $logger */ public function __construct( UrlRewriteFactory $urlRewriteFactory, @@ -56,7 +59,7 @@ public function __construct( $this->connection = $resource->getConnection(); $this->resource = $resource; $this->logger = $logger ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Psr\Log\LoggerInterface::class); + ->get(LoggerInterface::class); parent::__construct($urlRewriteFactory, $dataObjectHelper); } @@ -97,42 +100,18 @@ protected function doFindOneByData(array $data) $result = null; $requestPath = $data[UrlRewrite::REQUEST_PATH]; - - $data[UrlRewrite::REQUEST_PATH] = [ + $decodedRequestPath = urldecode($requestPath); + $data[UrlRewrite::REQUEST_PATH] = array_unique([ rtrim($requestPath, '/'), rtrim($requestPath, '/') . '/', - ]; + rtrim($decodedRequestPath, '/'), + rtrim($decodedRequestPath, '/') . '/', + ]); $resultsFromDb = $this->connection->fetchAll($this->prepareSelect($data)); - - if (count($resultsFromDb) === 1) { - $resultFromDb = current($resultsFromDb); - $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; - - // If request path matches the DB value or it's redirect - we can return result from DB - $canReturnResultFromDb = ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath - || in_array((int)$resultFromDb[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true)); - - // Otherwise return 301 redirect to request path from DB results - $result = $canReturnResultFromDb ? $resultFromDb : [ - UrlRewrite::ENTITY_TYPE => 'custom', - UrlRewrite::ENTITY_ID => '0', - UrlRewrite::REQUEST_PATH => $requestPath, - UrlRewrite::TARGET_PATH => $resultFromDb[UrlRewrite::REQUEST_PATH], - UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, - UrlRewrite::STORE_ID => $resultFromDb[UrlRewrite::STORE_ID], - UrlRewrite::DESCRIPTION => null, - UrlRewrite::IS_AUTOGENERATED => '0', - UrlRewrite::METADATA => null, - ]; - } else { - // If we have 2 results - return the row that matches request path - foreach ($resultsFromDb as $resultFromDb) { - if ($resultFromDb[UrlRewrite::REQUEST_PATH] === $requestPath) { - $result = $resultFromDb; - break; - } - } + if ($resultsFromDb) { + $urlRewrite = $this->extractMostRelevantUrlRewrite($requestPath, $resultsFromDb); + $result = $this->prepareUrlRewrite($requestPath, $urlRewrite); } return $result; @@ -142,8 +121,78 @@ protected function doFindOneByData(array $data) } /** - * @param UrlRewrite[] $urls + * Extract most relevant url rewrite from url rewrites list + * + * @param string $requestPath + * @param array $urlRewrites + * @return array|null + */ + private function extractMostRelevantUrlRewrite(string $requestPath, array $urlRewrites) + { + $prioritizedUrlRewrites = []; + foreach ($urlRewrites as $urlRewrite) { + switch (true) { + case $urlRewrite[UrlRewrite::REQUEST_PATH] === $requestPath: + $priority = 1; + break; + case $urlRewrite[UrlRewrite::REQUEST_PATH] === urldecode($requestPath): + $priority = 2; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim($requestPath, '/'): + $priority = 3; + break; + case rtrim($urlRewrite[UrlRewrite::REQUEST_PATH], '/') === rtrim(urldecode($requestPath), '/'): + $priority = 4; + break; + default: + $priority = 5; + break; + } + $prioritizedUrlRewrites[$priority] = $urlRewrite; + } + ksort($prioritizedUrlRewrites); + + return array_shift($prioritizedUrlRewrites); + } + + /** + * Prepare url rewrite * + * If request path matches the DB value or it's redirect - we can return result from DB + * Otherwise return 301 redirect to request path from DB results + * + * @param string $requestPath + * @param array $urlRewrite + * @return array + */ + private function prepareUrlRewrite(string $requestPath, array $urlRewrite): array + { + $redirectTypes = [OptionProvider::TEMPORARY, OptionProvider::PERMANENT]; + $canReturnResultFromDb = ( + in_array($urlRewrite[UrlRewrite::REQUEST_PATH], [$requestPath, urldecode($requestPath)], true) + || in_array((int) $urlRewrite[UrlRewrite::REDIRECT_TYPE], $redirectTypes, true) + ); + if (!$canReturnResultFromDb) { + $urlRewrite = [ + UrlRewrite::ENTITY_TYPE => 'custom', + UrlRewrite::ENTITY_ID => '0', + UrlRewrite::REQUEST_PATH => $requestPath, + UrlRewrite::TARGET_PATH => $urlRewrite[UrlRewrite::REQUEST_PATH], + UrlRewrite::REDIRECT_TYPE => OptionProvider::PERMANENT, + UrlRewrite::STORE_ID => $urlRewrite[UrlRewrite::STORE_ID], + UrlRewrite::DESCRIPTION => null, + UrlRewrite::IS_AUTOGENERATED => '0', + UrlRewrite::METADATA => null, + ]; + } + + return $urlRewrite; + } + + /** + * Delete old url rewrites + * + * @param UrlRewrite[] $urls * @return void */ private function deleteOldUrls(array $urls) diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteEditPage.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteEditPage.xml new file mode 100644 index 0000000000000..f51b9321e6911 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Page/AdminUrlRewriteEditPage.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminUrlRewriteEditPage" url="admin/url_rewrite/edit/id/{{url_rewrite_id}}/" area="admin" module="Magento_UrlRewrite" parameterized="true"> + <section name="AdminUrlRewriteEditSection"/> + </page> +</pages> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteEditSection.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteEditSection.xml new file mode 100644 index 0000000000000..7095214e72c43 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Section/AdminUrlRewriteEditSection.xml @@ -0,0 +1,14 @@ +<?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="AdminUrlRewriteEditSection"> + <element name="requestPathField" type="input" selector="#request_path"/> + </section> +</sections> diff --git a/app/code/Magento/Variable/view/adminhtml/web/variables.js b/app/code/Magento/Variable/view/adminhtml/web/variables.js index d519053b5265a..e91f172f59fe7 100644 --- a/app/code/Magento/Variable/view/adminhtml/web/variables.js +++ b/app/code/Magento/Variable/view/adminhtml/web/variables.js @@ -9,7 +9,8 @@ define([ 'mage/translate', 'Magento_Ui/js/modal/modal', 'jquery/ui', - 'prototype' + 'prototype', + 'mage/adminhtml/tools' ], function (jQuery, $t) { 'use strict'; diff --git a/app/code/Magento/Vault/view/frontend/web/template/payment/form.html b/app/code/Magento/Vault/view/frontend/web/template/payment/form.html index 0ef330cd3014e..49cc488060120 100644 --- a/app/code/Magento/Vault/view/frontend/web/template/payment/form.html +++ b/app/code/Magento/Vault/view/frontend/web/template/payment/form.html @@ -19,7 +19,8 @@ <img data-bind="attr: { 'src': getIcons(getCardType()).url, 'width': getIcons(getCardType()).width, - 'height': getIcons(getCardType()).height + 'height': getIcons(getCardType()).height, + 'alt': getIcons(getCardType()).title }" class="payment-icon"> <span translate="'ending'"></span> <span text="getMaskedCard()"></span> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml index dfb33b197d74f..5fc5bc4a56abf 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -32,6 +32,8 @@ <click selector="{{AdminNewWidgetSection.addNewCondition}}" stepKey="clickAddNewCondition"/> <selectOption selector="{{AdminNewWidgetSection.selectCondition}}" userInput="{{widget.condition}}" stepKey="selectCondition"/> <waitForElement selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="waitRuleParameter"/> + <click selector="{{AdminNewWidgetSection.ruleParameterEdit}}" stepKey="clickRuleParameterEdit"/> + <selectOption selector="{{AdminNewWidgetSection.ruleParameterEditSelect}}" userInput="{{widget.ruleIsOneOf}}" stepKey="selectRuleParameterEdit"/> <click selector="{{AdminNewWidgetSection.ruleParameter}}" stepKey="clickRuleParameter"/> <click selector="{{AdminNewWidgetSection.openChooser}}" stepKey="clickChooser"/> <waitForPageLoad stepKey="waitForAjaxLoad"/> @@ -63,4 +65,17 @@ <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="confirmDelete"/> <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been deleted" stepKey="seeSuccess"/> </actionGroup> + <actionGroup name="AdminCreateProductLinkWidgetActionGroup" extends="AdminCreateWidgetActionGroup"> + <arguments> + <argument name="product"/> + </arguments> + <selectOption selector="{{AdminNewWidgetSection.selectTemplate}}" userInput="{{widget.template}}" after="waitForPageLoad" stepKey="setTemplate"/> + <waitForAjaxLoad after="setTemplate" stepKey="waitForPageLoad2"/> + <click selector="{{AdminNewWidgetSection.selectProduct}}" after="clickWidgetOptions" stepKey="clickSelectProduct"/> + <fillField selector="{{AdminNewWidgetSelectProductPopupSection.filterBySku}}" userInput="{{product.sku}}" after="clickSelectProduct" stepKey="fillProductNameInFilter"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" after="fillProductNameInFilter" stepKey="applyFilter"/> + <click selector="{{AdminNewWidgetSelectProductPopupSection.firstRow}}" after="applyFilter" stepKey="selectProduct"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveWidget"/> + <see selector="{{AdminMessagesSection.successMessage}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml index d7db8fe50cb7f..ce56a6d92dd97 100644 --- a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml +++ b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml @@ -18,6 +18,7 @@ <data key="condition">SKU</data> <data key="display_on">All Pages</data> <data key="container">Main Content Area</data> + <data key="ruleIsOneOf">is one of</data> </entity> <entity name="DynamicBlocksRotatorWidget" type="widget"> <data key="type">Banner Rotator</data> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index 37ad425114d5c..c8c0d0abc3576 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -17,9 +17,12 @@ <element name="addLayoutUpdate" type="button" selector=".action-default.scalable.action-add"/> <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="selectTemplate" type="select" selector=".widget-layout-updates .block_template_container .select"/> <element name="widgetOptions" type="select" selector="#widget_instace_tabs_properties_section"/> <element name="addNewCondition" type="select" selector=".rule-param.rule-param-new-child"/> <element name="selectCondition" type="input" selector="#conditions__1__new_child"/> + <element name="ruleParameterEdit" type="select" selector="#conditions__1__children>li:nth-child(1)>span:nth-child(3)>a"/> + <element name="ruleParameterEditSelect" type="select" selector="#conditions__1__children>li:nth-child(1)>span:nth-child(3)>span>select"/> <element name="ruleParameter" type="select" selector="#conditions__1__children>li:nth-child(1)>span:nth-child(4)>a"/> <element name="setRuleParameter" type="input" selector="#conditions__1--1__value"/> <element name="applyParameter" type="button" selector=".rule-param-apply"/> diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php index cafb6a5291481..40882ae00dae1 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php @@ -5,14 +5,15 @@ */ /** - * Wishlist for item column in customer wishlist - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Model for item column in customer wishlist. + * * @api + * @deprecated * @since 100.0.2 */ class Actions extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php index 2d75956858a0a..53f67626e956d 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php @@ -5,14 +5,15 @@ */ /** - * Wishlist block customer item cart column - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Wishlist block customer item cart column. + * * @api + * @deprecated * @since 100.0.2 */ class Comment extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php index 53ca78c63524d..c4c786961694b 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php @@ -5,14 +5,15 @@ */ /** - * Edit item in customer wishlist table - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Edit item in customer wishlist table. + * * @api + * @deprecated * @since 100.0.2 */ class Edit extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php index 33fb0f7325cdd..b7eaf53fc23b5 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php @@ -5,14 +5,15 @@ */ /** - * Wishlist block customer item cart column - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Wishlist block customer item cart column. + * * @api + * @deprecated * @since 100.0.2 */ class Info extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php index 57703b9300db8..09f5014edead6 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php @@ -5,14 +5,15 @@ */ /** - * Delete item column in customer wishlist table - * * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\Wishlist\Block\Customer\Wishlist\Item\Column; /** + * Delete item column in customer wishlist table + * * @api + * @deprecated * @since 100.0.2 */ class Remove extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php new file mode 100644 index 0000000000000..fb0113eb6ae75 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Unit/Model/Product/AttributeValueProviderTest.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Test\Unit\Model\Product; + +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Wishlist\Model\Product\AttributeValueProvider; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +/** + * AttributeValueProviderTest + */ +class AttributeValueProviderTest extends TestCase +{ + /** + * @var AttributeValueProvider|PHPUnit_Framework_MockObject_MockObject + */ + private $attributeValueProvider; + + /** + * @var CollectionFactory|PHPUnit_Framework_MockObject_MockObject + */ + private $productCollectionFactoryMock; + + /** + * @var Product|PHPUnit_Framework_MockObject_MockObject + */ + private $productMock; + + /** + * @var AdapterInterface|PHPUnit_Framework_MockObject_MockObject + */ + private $connectionMock; + + /** + * Set Up + * + * @return void + */ + protected function setUp() + { + $this->productCollectionFactoryMock = $this->createPartialMock( + CollectionFactory::class, + ['create'] + ); + $this->attributeValueProvider = new AttributeValueProvider( + $this->productCollectionFactoryMock + ); + } + + /** + * Get attribute text when the flat table is disabled + * + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + * @dataProvider attributeDataProvider + */ + public function testGetAttributeTextWhenFlatIsDisabled(int $productId, string $attributeCode, string $attributeText) + { + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getFirstItem' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(false); + $productCollection->expects($this->any()) + ->method('getFirstItem') + ->willReturn($this->productMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * Get attribute text when the flat table is enabled + * + * @dataProvider attributeDataProvider + * @param int $productId + * @param string $attributeCode + * @param string $attributeText + * @return void + */ + public function testGetAttributeTextWhenFlatIsEnabled(int $productId, string $attributeCode, string $attributeText) + { + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass(); + $this->connectionMock->expects($this->any()) + ->method('fetchRow') + ->willReturn([ + $attributeCode => $attributeText + ]); + $this->productMock = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getData']) + ->getMock(); + $this->productMock->expects($this->any()) + ->method('getData') + ->with($attributeCode) + ->willReturn($attributeText); + + $productCollection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->setMethods([ + 'addIdFilter', 'addStoreFilter', 'addAttributeToSelect', 'isEnabledFlat', 'getConnection' + ])->getMock(); + + $productCollection->expects($this->any()) + ->method('addIdFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addStoreFilter') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $productCollection->expects($this->any()) + ->method('isEnabledFlat') + ->willReturn(true); + $productCollection->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connectionMock); + + $this->productCollectionFactoryMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturn($productCollection); + + $actual = $this->attributeValueProvider->getRawAttributeValue($productId, $attributeCode); + + $this->assertEquals($attributeText, $actual); + } + + /** + * @return array + */ + public function attributeDataProvider(): array + { + return [ + [1, 'attribute_code', 'Attribute Text'] + ]; + } +} diff --git a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml index 243a06062425a..218b5ac1c879b 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_index.xml @@ -17,6 +17,7 @@ <block class="Magento\Wishlist\Block\Customer\Wishlist\Items" name="customer.wishlist.items" as="items" template="Magento_Wishlist::item/list.phtml" cacheable="false"> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Image" name="customer.wishlist.item.image" template="Magento_Wishlist::item/column/image.phtml" cacheable="false"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Info" name="customer.wishlist.item.name" template="Magento_Wishlist::item/column/name.phtml" cacheable="false"/> + <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column" name="customer.wishlist.item.review" template="Magento_Wishlist::item/column/review.phtml" cacheable="false"/> <block class="Magento\Wishlist\Block\Customer\Wishlist\Item\Column\Cart" name="customer.wishlist.item.price" template="Magento_Wishlist::item/column/price.phtml" cacheable="false"> <block class="Magento\Catalog\Pricing\Render" name="product.price.render.wishlist"> <arguments> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml b/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml new file mode 100644 index 0000000000000..3fd492233bdd5 --- /dev/null +++ b/app/code/Magento/Wishlist/view/frontend/templates/item/column/review.phtml @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Wishlist\Block\Customer\Wishlist\Item\Column $block */ +$product = $block->getItem()->getProduct(); +?> +<?= $block->getReviewsSummaryHtml($product, 'short'); diff --git a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less index 0bd6cc62bd509..1bf1c51f54976 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less +++ b/app/design/adminhtml/Magento/backend/Magento_Backend/web/css/source/module/main/actions-bar/_store-switcher.less @@ -236,6 +236,7 @@ .store-view { &:not(.store-switcher) { float: left; + margin-top: 13px; } .store-switcher-label { diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less index e14bcbcddd47f..5c30a00c9a0d7 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_order-account.less @@ -27,3 +27,22 @@ width: 50%; } } + +.page-create-order { + .order-details { + &:not(.order-details-existing-customer) { + .order-account-information { + .field-email { + margin-left: -30px; + } + /** + * @codingStandardsIgnoreStart + */ + .field-group_id { + margin-right: 30px; + } + // @codingStandardsIgnoreEnd + } + } + } +} diff --git a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-multiselect.less b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-multiselect.less index 213dd3d0f8e25..fc56ec1b6570a 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-multiselect.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/actions/_actions-multiselect.less @@ -330,7 +330,7 @@ border-top: @action-multiselect-tree-lines; height: 1px; top: @action-multiselect-menu-item__padding + @action-multiselect-tree-arrow__size/2; - width: @action-multiselect-tree-menu-item__margin-left + @action-multiselect-menu-item__padding; + width: @action-multiselect-tree-menu-item__margin-left; } // Vertical dotted line diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index 41aff7796090d..5879c397e6bee 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less @@ -527,7 +527,6 @@ & > .admin__field-label { #mix-grid .column(@field-label-grid__column, @field-grid__columns); - background: @color-white; cursor: pointer; left: 0; position: absolute; diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less index 4479c070a4e17..8dec680b58726 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_fields.less @@ -55,31 +55,3 @@ } } } - -// -// Desktop -// _____________________________________________ - -.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { - // ToDo UI: remove with global blank theme .field.required update - .opc-wrapper { - .fieldset { - > .field { - &.required, - &._required { - position: relative; - - > label { - padding-right: 25px; - - &:after { - margin-left: @indent__s; - position: absolute; - top: 9px; - } - } - } - } - } - } -} diff --git a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less index 5f8134193c67f..35445b0989e86 100644 --- a/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/blank/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -209,6 +209,13 @@ .fieldset { > .field { margin: 0 0 @indent__base; + + &.choice { + &:before { + padding: 0; + width: 0; + } + } &.type { .control { diff --git a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less index 1ea1e2c483d0b..44eb06ac5ce19 100644 --- a/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Sales/web/css/source/_module.less @@ -294,6 +294,14 @@ } } } + .order-items.table-wrapper { + .col.price, + .col.qty, + .col.subtotal, + .col.msrp { + text-align: left; + } + } } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { diff --git a/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less index eeb17653c877b..ac415f007f86e 100644 --- a/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Bundle/web/css/source/_module.less @@ -58,6 +58,11 @@ .field.choice { input { float: left; + margin-top: 4px; + } + + input[type='checkbox'] { + margin-top: 2px; } .label { diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index 6aa0e50f3c393..fca906d432790 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -294,6 +294,11 @@ } .product-options-wrapper { + .fieldset { + &:focus { + box-shadow: none; + } + } .fieldset-product-options-inner { .legend { .lib-css(font-weight, @font-weight__semibold); @@ -534,6 +539,15 @@ } } + .block-compare { + .action { + &.delete { + &:extend(.abs-remove-button-for-blocks all); + right: initial; + } + } + } + .action.tocart { border-radius: 0; } diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 67057967451d8..3fef5b303d718 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less @@ -422,6 +422,14 @@ &:extend(.abs-sidebar-totals-mobile all); } } + .order-items.table-wrapper { + .col.price, + .col.qty, + .col.subtotal, + .col.msrp { + text-align: left; + } + } } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { @@ -501,6 +509,17 @@ } } } + + .cart.table-wrapper, + .order-items.table-wrapper { + .col.price, + .col.qty, + .col.subtotal, + .col.msrp { + text-align: left; + } + } + } // diff --git a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less index 088372808aa6a..fe49d6679a613 100644 --- a/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_GroupedProduct/web/css/source/_module.less @@ -133,9 +133,18 @@ } .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { - .table-wrapper.grouped { - .lib-css(margin-left, -@layout__width-xs-indent); - .lib-css(margin-right, -@layout__width-xs-indent); + .product-add-form { + .table-wrapper.grouped { + .lib-css(margin-left, -@layout__width-xs-indent); + .lib-css(margin-right, -@layout__width-xs-indent); + .table.data.grouped { + tr { + td { + padding: 5px 10px 5px 15px; + } + } + } + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less index 9761f36b96344..7977fb16c524c 100644 --- a/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Multishipping/web/css/source/_module.less @@ -345,6 +345,22 @@ .data.table { &:extend(.abs-checkout-order-review all); + &.table-order-review { + > tbody { + > tr { + > td { + &.col { + &.subtotal { + border-bottom: none; + } + &.qty { + text-align: center; + } + } + } + } + } + } } } diff --git a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less index 9ccd6c190ec0e..d7ee1319c9a43 100644 --- a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less @@ -81,3 +81,24 @@ width: 34%; } } + +// +// Mobile +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .block { + &.newsletter { + input { + font-size: 12px; + padding-left: 30px; + } + + .field { + .control:before { + font-size: 13px; + } + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less index 1e4a92fa0701f..314f0d0ee6298 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less @@ -555,13 +555,13 @@ margin: 0 @tab-control__margin-right 0 0; a { - padding: @tab-control__padding-top @tab-control__padding-right; + padding: @tab-control__padding-top @indent__base; } strong { border-bottom: 0; margin-bottom: -1px; - padding: @tab-control__padding-top @tab-control__padding-right @tab-control__padding-bottom + 1 @tab-control__padding-left; + padding: @tab-control__padding-top @indent__base @tab-control__padding-bottom + 1 @indent__base; } } } @@ -687,3 +687,19 @@ } } } + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__l) { + .order-links { + .item { + margin: 0 @tab-control__margin-right 0 0; + + a { + padding: @tab-control__padding-top @tab-control__padding-right; + } + + strong { + padding: @tab-control__padding-top @tab-control__padding-right @tab-control__padding-bottom + 1 @tab-control__padding-left; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less index baf5468b18485..3435736a54a6a 100644 --- a/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_SendFriend/web/css/source/_module.less @@ -10,6 +10,14 @@ & when (@media-common = true) { .form.send.friend { &:extend(.abs-add-fields all); + + .fieldset { + .field { + .control { + width: 100%; + } + } + } } .product-social-links .action.mailto.friend { @@ -44,3 +52,18 @@ } } } +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .form.send.friend { + .fieldset { + padding-bottom: @indent__xs; + } + + .action { + &.remove { + margin-left: 0; + right: 0; + top: 100%; + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less index f3e0f810481ed..c5ad329a17c52 100644 --- a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/_module.less @@ -464,7 +464,7 @@ .media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { .cms-page-view .page-main { - padding-top: 41px; + padding-top: 0; position: relative; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php index 1927d967a181c..e99a892b184ec 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductCustomOptionRepositoryTest.php @@ -5,32 +5,41 @@ * See COPYING.txt for license details. */ -// @codingStandardsIgnoreFile - namespace Magento\Catalog\Api; +use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; class ProductCustomOptionRepositoryTest extends WebapiAbstract { /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $objectManager; const SERVICE_NAME = 'catalogProductCustomOptionRepositoryV1'; /** - * @var \Magento\Catalog\Model\ProductFactory + * @var ProductFactory */ protected $productFactory; + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @inheritdoc + */ protected function setUp() { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->productFactory = $this->objectManager->get(\Magento\Catalog\Model\ProductFactory::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productFactory = $this->objectManager->get(ProductFactory::class); + $this->productRepository = $this->objectManager->create(ProductRepository::class); } /** @@ -40,12 +49,8 @@ protected function setUp() public function testRemove() { $sku = 'simple'; - /** @var ProductRepository $productRepository */ - $productRepository = $this->objectManager->create( - \Magento\Catalog\Model\ProductRepository::class - ); - /** @var \Magento\Catalog\Model\Product $product */ - $product = $productRepository->get($sku, false, null, true); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get($sku, false, null, true); $customOptions = $product->getOptions(); $optionId = array_pop($customOptions)->getId(); $serviceInfo = [ @@ -60,8 +65,8 @@ public function testRemove() ], ]; $this->assertTrue($this->_webApiCall($serviceInfo, ['sku' => $sku, 'optionId' => $optionId])); - /** @var \Magento\Catalog\Model\Product $product */ - $product = $productRepository->get($sku, false, null, true); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get($sku, false, null, true); $this->assertNull($product->getOptionById($optionId)); $this->assertEquals(9, count($product->getOptions())); } @@ -164,6 +169,7 @@ public function testSave($optionData) ]; $result = $this->_webApiCall($serviceInfo, ['option' => $optionDataPost]); + $product = $this->productRepository->get($productSku); unset($result['product_sku']); unset($result['option_id']); if (!empty($result['values'])) { @@ -172,6 +178,10 @@ public function testSave($optionData) } } $this->assertEquals($optionData, $result); + $this->assertEquals(1, $product->getHasOptions()); + if ($optionDataPost['is_require']) { + $this->assertEquals(1, $product->getRequiredOptions()); + } } public function optionDataProvider() @@ -182,7 +192,7 @@ public function optionDataProvider() $fixtureOptions[$item['type']] = [ 'optionData' => $item, ]; - }; + } return $fixtureOptions; } @@ -191,8 +201,9 @@ public function optionDataProvider() * @magentoApiDataFixture Magento/Catalog/_files/product_without_options.php * @magentoAppIsolation enabled * @dataProvider optionNegativeDataProvider + * @param array $optionData */ - public function testAddNegative($optionData) + public function testAddNegative(array $optionData) { $productSku = 'simple'; $optionDataPost = $optionData; @@ -244,12 +255,7 @@ public function optionNegativeDataProvider() public function testUpdate() { $productSku = 'simple'; - /** @var ProductRepository $productRepository */ - $productRepository = $this->objectManager->create( - \Magento\Catalog\Model\ProductRepository::class - ); - - $options = $productRepository->get($productSku, true)->getOptions(); + $options = $this->productRepository->get($productSku, true)->getOptions(); $option = array_shift($options); $optionId = $option->getOptionId(); $optionDataPost = [ @@ -292,14 +298,13 @@ public function testUpdate() } /** - * @param string $optionType - * * @magentoApiDataFixture Magento/Catalog/_files/product_with_options.php * @magentoAppIsolation enabled * @dataProvider validOptionDataProvider + * @param string $optionType * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - public function testUpdateOptionAddingNewValue($optionType) + public function testUpdateOptionAddingNewValue(string $optionType) { $fixtureOption = null; $valueData = [ @@ -310,14 +315,10 @@ public function testUpdateOptionAddingNewValue($optionType) 'sort_order' => 100, ]; - /** @var ProductRepository $productRepository */ - $productRepository = $this->objectManager->create( - \Magento\Catalog\Model\ProductRepository::class - ); - /** @var \Magento\Catalog\Model\Product $product */ - $product = $productRepository->get('simple', false, null, true); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get('simple', false, null, true); - /**@var $option \Magento\Catalog\Model\Product\Option */ + /** @var \Magento\Catalog\Model\Product\Option $option */ foreach ($product->getOptions() as $option) { if ($option->getType() == $optionType) { $fixtureOption = $option; @@ -361,7 +362,7 @@ public function testUpdateOptionAddingNewValue($optionType) $data['option_id'] = $fixtureOption->getId(); $valueObject = $this->_webApiCall( $serviceInfo, - [ 'option_id' => $fixtureOption->getId(), 'option' => $data] + ['option_id' => $fixtureOption->getId(), 'option' => $data] ); } else { $valueObject = $this->_webApiCall($serviceInfo, ['option' => $data]); @@ -397,9 +398,7 @@ public function testUpdateNegative($optionData, $message, $exceptionCode) { $this->_markTestAsRestOnly(); $productSku = 'simple'; - /** @var ProductRepository $productRepository */ - $productRepository = $this->objectManager->create(ProductRepository::class); - $options = $productRepository->get($productSku, true)->getOptions(); + $options = $this->productRepository->get($productSku, true)->getOptions(); $option = array_shift($options); $optionId = $option->getOptionId(); diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php index 6dbf2b1aa6a12..9edd087020a72 100644 --- a/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/ConditionsElement.php @@ -195,6 +195,13 @@ class ConditionsElement extends SimpleElement */ protected $exception; + /** + * Condition option text selector. + * + * @var string + */ + private $conditionOptionTextSelector = '//option[normalize-space(text())="%s"]'; + /** * @inheritdoc */ @@ -282,10 +289,16 @@ protected function addCondition($type, ElementInterface $context) $count = 0; do { - $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); - try { - $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select')->setValue($type); + $specificType = $newCondition->find( + sprintf($this->conditionOptionTextSelector, $type), + Locator::SELECTOR_XPATH + )->isPresent(); + $newCondition->find($this->addNew, Locator::SELECTOR_XPATH)->click(); + $condition = $specificType + ? $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'selectcondition') + : $newCondition->find($this->typeNew, Locator::SELECTOR_XPATH, 'select'); + $condition->setValue($type); $isSetType = true; } catch (\PHPUnit_Extensions_Selenium2TestCase_WebDriverException $e) { $isSetType = false; diff --git a/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php new file mode 100644 index 0000000000000..15a799eac5188 --- /dev/null +++ b/dev/tests/functional/lib/Magento/Mtf/Client/Element/SelectconditionElement.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Mtf\Client\Element; + +/** + * @inheritdoc + */ +class SelectconditionElement extends SelectElement +{ + /** + * @inheritdoc + */ + protected $optionByValue = './/option[normalize-space(.)=%s]'; +} diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml index a66753c2adf23..9614f0d1bf7b5 100644 --- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml +++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Block/Adminhtml/Product/Composite/Configure.xml @@ -9,7 +9,7 @@ <fields> <qty /> <attribute> - <selector>//div[@class="product-options"]//label[.="%s"]//following-sibling::*//select</selector> + <selector>//div[contains(@class, "product-options")]//div//label[.="%s"]//following-sibling::*//select</selector> <strategy>xpath</strategy> <input>select</input> </attribute> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml index 586ad2acee203..4995c1feb048e 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/TestCase/CreateSalesRuleEntityTest.xml @@ -75,7 +75,6 @@ <data name="salesRule/data/coupon_code" xsi:type="string">Lorem ipsum dolor sit amet, consectetur adipiscing elit - %isolation%</data> <data name="salesRule/data/simple_action" xsi:type="string">Fixed amount discount for whole cart</data> <data name="salesRule/data/discount_amount" xsi:type="string">60</data> - <data name="salesRule/data/apply_to_shipping" xsi:type="string">No</data> <data name="salesRule/data/simple_free_shipping" xsi:type="string">No</data> <data name="salesRule/data/store_labels/0" xsi:type="string">Coupon code+Fixed amount discount for whole cart</data> <data name="productForSalesRule1/dataset" xsi:type="string">simple_for_salesrule_1</data> diff --git a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php index 07af21505f180..20fbe2a3f3f62 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Controller/Adminhtml/DashboardTest.php @@ -21,6 +21,8 @@ public function testAjaxBlockAction() public function testTunnelAction() { + $this->markTestSkipped('MAGETWO-98803'); + $testUrl = \Magento\Backend\Block\Dashboard\Graph::API_URL . '?cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'; $handle = curl_init(); curl_setopt($handle, CURLOPT_URL, $testUrl); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php index 7954e2c36227f..476f01eb277df 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/ProductTest.php @@ -12,6 +12,11 @@ class ProductTest extends TestCase { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @var Product */ @@ -29,7 +34,8 @@ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $this->model = $this->objectManager->get(Product::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->model = $this->objectManager->create(Product::class); } /** @@ -42,11 +48,29 @@ public function testGetAttributeRawValue() $sku = 'simple'; $attribute = 'name'; - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($sku); - + $product = $this->productRepository->get($sku); $actual = $this->model->getAttributeRawValue($product->getId(), $attribute, null); self::assertEquals($product->getName(), $actual); } + + /** + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoAppIsolation enabled + * @magentoConfigFixture default_store catalog/price/scope 1 + */ + public function testUpdateStoreSpecificSpecialPrice() + { + /** @var \Magento\Catalog\Model\Product $product */ + $product = $this->productRepository->get('simple', true, 1); + $this->assertEquals(5.99, $product->getSpecialPrice()); + + $product->setSpecialPrice(''); + $this->model->save($product); + $product = $this->productRepository->get('simple', false, 1, true); + $this->assertEmpty($product->getSpecialPrice()); + + $product = $this->productRepository->get('simple', false, 0, true); + $this->assertEquals(5.99, $product->getSpecialPrice()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/QuantityAndStockStatusTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/QuantityAndStockStatusTest.php new file mode 100644 index 0000000000000..00fc5d3a46ec4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/QuantityAndStockStatusTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Ui\DataProvider\Product; + +use Magento\CatalogInventory\Model\Stock\StockItemRepository; +use Magento\CatalogInventory\Ui\DataProvider\Product\AddQuantityAndStockStatusFieldToCollection; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\CatalogInventory\Api\StockRegistryInterface; + +/** + * Quantity and stock status test + */ +class QuantityAndStockStatusTest extends TestCase +{ + /** + * @var string + */ + private static $quantityAndStockStatus = 'quantity_and_stock_status'; + + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Test product stock status in the products grid column + * + * @magentoDataFixture Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid.php + * @magentoDataFixture Magento/Catalog/_files/products.php + */ + public function testProductStockStatus() + { + /** @var StockItemRepository $stockItemRepository */ + $stockItemRepository = $this->objectManager->create(StockItemRepository::class); + + /** @var StockRegistryInterface $stockRegistry */ + $stockRegistry = $this->objectManager->create(StockRegistryInterface::class); + + $stockItem = $stockRegistry->getStockItemBySku('simple'); + $stockItem->setIsInStock(false); + $stockItemRepository->save($stockItem); + $savedStockStatus = (int)$stockItem->getIsInStock(); + + $dataProvider = $this->objectManager->create( + ProductDataProvider::class, + [ + 'name' => 'product_listing_data_source', + 'primaryFieldName' => 'entity_id', + 'requestFieldName' => 'id', + 'addFieldStrategies' => [ + 'quantity_and_stock_status' => + $this->objectManager->get(AddQuantityAndStockStatusFieldToCollection::class) + ] + ] + ); + + $dataProvider->addField(self::$quantityAndStockStatus); + $data = $dataProvider->getData(); + $dataProviderStockStatus = $data['items'][0][self::$quantityAndStockStatus]; + + $this->assertEquals($dataProviderStockStatus, $savedStockStatus); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid.php new file mode 100644 index 0000000000000..1870eaba566d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$eavSetupFactory = $objectManager->create(\Magento\Eav\Setup\EavSetupFactory::class); +/** @var \Magento\Eav\Setup\EavSetup $eavSetup */ +$eavSetup = $eavSetupFactory->create(); +$eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'quantity_and_stock_status', + [ + 'is_used_in_grid' => 1, + ] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid_rollback.php new file mode 100644 index 0000000000000..fba12f19fdca8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/quantity_and_stock_status_attribute_used_in_grid_rollback.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$eavSetupFactory = $objectManager->create(\Magento\Eav\Setup\EavSetupFactory::class); +/** @var \Magento\Eav\Setup\EavSetup $eavSetup */ +$eavSetup = $eavSetupFactory->create(); +$eavSetup->updateAttribute( + \Magento\Catalog\Model\Product::ENTITY, + 'quantity_and_stock_status', + [ + 'is_used_in_grid' => 0, + ] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_products_not_visible_individually.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_products_not_visible_individually.php new file mode 100644 index 0000000000000..dfb8ce4d4741f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_products_not_visible_individually.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(67) + ->setAttributeSetId(4) + ->setName('Simple Product Not visible 1') + ->setSku('simple_not_visible_1') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_IN_CART) + ->setPrice(10) + ->setWeight(1) + ->setMetaTitle('meta title product Not visible') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([300]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 30, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('5.99') + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_products_not_visible_individually_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_products_not_visible_individually_rollback.php new file mode 100644 index 0000000000000..bbdcfd7acf081 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_products_not_visible_individually_rollback.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Exception\NoSuchEntityException; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +try { + $product = $productRepository->get('simple_not_visible_1', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { +} +$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 9cc7c452e646f..6e361dfb05de0 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -1388,10 +1388,15 @@ public function testValidateUrlKeys($importFile, $expectedErrors) )->setSource( $source )->validateData(); + $urlKeyErrors = $errors->getErrorsByCode([RowValidatorInterface::ERROR_DUPLICATE_URL_KEY]); $this->assertEquals( $expectedErrors[RowValidatorInterface::ERROR_DUPLICATE_URL_KEY], - count($errors->getErrorsByCode([RowValidatorInterface::ERROR_DUPLICATE_URL_KEY])) + count($urlKeyErrors) ); + + foreach ($urlKeyErrors as $error) { + $this->assertEquals('critical', $error->getErrorLevel()); + } } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php b/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php index 08e9ebbd1f9f0..5774f1cf76ae9 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogWidget/Block/Product/ProductListTest.php @@ -73,21 +73,6 @@ public function testCreateCollection() $this->performAssertions(2); } - /** - * Test product list widget can process condition with multiple product sku. - * - * @magentoDbIsolation disabled - * @magentoDataFixture Magento/Catalog/_files/multiple_products.php - */ - public function testCreateCollectionWithMultipleSkuCondition() - { - $encodedConditions = '^[`1`:^[`type`:`Magento||CatalogWidget||Model||Rule||Condition||Combine`,' . - '`aggregator`:`all`,`value`:`1`,`new_child`:``^],`1--1`:^[`type`:`Magento||CatalogWidget||Model||Rule|' . - '|Condition||Product`,`attribute`:`sku`,`operator`:`==`,`value`:`simple1, simple2`^]^]'; - $this->block->setData('conditions_encoded', $encodedConditions); - $this->performAssertions(2); - } - /** * Test product list widget can process condition with dropdown type of attribute * diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php index 43108dbca1f5e..85dede0d84c2d 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Plugin/Model/Quote/ResetQuoteAddressesTest.php @@ -21,6 +21,7 @@ class ResetQuoteAddressesTest extends \PHPUnit\Framework\TestCase { /** * @magentoDataFixture Magento/Checkout/_files/quote_with_virtual_product_and_address.php + * @magentoAppArea frontend * * @return void */ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php index 234f0aae6a3ea..63858e91b64f2 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProductsTest.php @@ -5,7 +5,19 @@ */ namespace Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\Data; -class AssociatedProductsTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\ConfigurableProduct\Ui\DataProvider\Product\Form\Modifier\ConfigurablePanel; +use Magento\Framework\App\RequestInterface; +use PHPUnit\Framework\TestCase; + +/** + * AssociatedProductsTest + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class AssociatedProductsTest extends TestCase { /** * @var \Magento\Framework\ObjectManagerInterface $objectManager @@ -17,6 +29,9 @@ class AssociatedProductsTest extends \PHPUnit\Framework\TestCase */ private $registry; + /** + * @inheritdoc + */ public function setUp() { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -64,6 +79,53 @@ public function testGetProductMatrix($interfaceLocale) } } + /** + * Tests configurable product won't appear in product listing. + * + * Tests configurable product won't appear in configurable associated product listing if its options attribute + * is not filterable in grid. + * + * @return void + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoAppArea adminhtml + */ + public function testAddManuallyConfigurationsWithNotFilterableInGridAttribute() + { + /** @var RequestInterface $request */ + $request = $this->objectManager->get(RequestInterface::class); + $request->setParams([ + FilterModifier::FILTER_MODIFIER => [ + 'test_configurable' => [ + 'condition_type' => 'notnull', + ], + ], + 'attributes_codes' => [ + 'test_configurable' + ], + ]); + $context = $this->objectManager->create(ContextInterface::class, ['request' => $request]); + /** @var UiComponentFactory $uiComponentFactory */ + $uiComponentFactory = $this->objectManager->get(UiComponentFactory::class); + $uiComponent = $uiComponentFactory->create( + ConfigurablePanel::ASSOCIATED_PRODUCT_LISTING, + null, + ['context' => $context] + ); + + foreach ($uiComponent->getChildComponents() as $childUiComponent) { + $childUiComponent->prepare(); + } + + $dataSource = $uiComponent->getDataSourceData(); + $skus = array_column($dataSource['data']['items'], 'sku'); + + $this->assertNotContains( + 'configurable', + $skus, + 'Only products with specified attribute should be in product list' + ); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php index 36df9cbf851bd..ccf9c45da8660 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php @@ -509,6 +509,42 @@ public function testSaveActionCoreException() $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'new/key/')); } + /** + * @magentoDataFixture Magento/Customer/_files/customer_sample.php + */ + public function testSaveActionCoreExceptionFormatFormData() + { + $post = [ + 'customer' => [ + 'website_id' => 1, + 'email' => 'customer@example.com', + 'dob' => '12/3/1996', + ], + ]; + $postFormatted = [ + 'customer' => [ + 'website_id' => 1, + 'email' => 'customer@example.com', + 'dob' => '1996-12-03', + ], + ]; + $this->getRequest()->setPostValue($post); + $this->dispatch('backend/customer/index/save'); + /* + * Check that error message is set + */ + $this->assertSessionMessages( + $this->equalTo(['A customer with the same email already exists in an associated website.']), + \Magento\Framework\Message\MessageInterface::TYPE_ERROR + ); + $this->assertEquals( + $postFormatted, + Bootstrap::getObjectManager()->get(\Magento\Backend\Model\Session::class)->getCustomerFormData(), + 'Customer form data should be formatted' + ); + $this->assertRedirect($this->stringStartsWith($this->_baseControllerUrl . 'new/key/')); + } + /** * @magentoDataFixture Magento/Customer/_files/customer_sample.php */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php new file mode 100644 index 0000000000000..aaa3aa6c97a7e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/Generator/AutoloaderTest.php @@ -0,0 +1,85 @@ +<?php declare(strict_types=1); +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Code\Generator; + +use Magento\Framework\Code\Generator; +use Magento\Framework\Logger\Monolog as MagentoMonologLogger; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Psr\Log\LoggerInterface; + +class AutoloaderTest extends TestCase +{ + /** + * This method exists to fix the wrong return type hint on \Magento\Framework\App\ObjectManager::getInstance. + * This way the IDE knows it's dealing with an instance of \Magento\TestFramework\ObjectManager and + * not \Magento\Framework\App\ObjectManager. The former has the method addSharedInstance, the latter does not. + * + * @return ObjectManager|\Magento\Framework\App\ObjectManager + * @SuppressWarnings(PHPMD.StaticAccess) + */ + private function getTestFrameworkObjectManager() + { + return ObjectManager::getInstance(); + } + + /** + * @before + */ + public function setupLoggerTestDouble() + { + $loggerTestDouble = $this->createMock(LoggerInterface::class); + $this->getTestFrameworkObjectManager()->addSharedInstance($loggerTestDouble, MagentoMonologLogger::class); + } + + /** + * @after + */ + public function removeLoggerTestDouble() + { + $this->getTestFrameworkObjectManager()->removeSharedInstance(MagentoMonologLogger::class); + } + + /** + * @param \RuntimeException $testException + * @return Generator|MockObject + */ + private function createExceptionThrowingGeneratorTestDouble(\RuntimeException $testException) + { + /** @var Generator|MockObject $generatorStub */ + $generatorStub = $this->createMock(Generator::class); + $generatorStub->method('generateClass')->willThrowException($testException); + + return $generatorStub; + } + + public function testLogsExceptionDuringGeneration() + { + $exceptionMessage = 'Test exception thrown during generation'; + $testException = new \RuntimeException($exceptionMessage); + + $loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class); + $loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]); + + $autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException)); + $this->assertNull($autoloader->load(NonExistingClassName::class)); + } + + public function testFiltersDuplicateExceptionMessages() + { + $exceptionMessage = 'Test exception thrown during generation'; + $testException = new \RuntimeException($exceptionMessage); + + $loggerMock = ObjectManager::getInstance()->get(LoggerInterface::class); + $loggerMock->expects($this->once())->method('debug')->with($exceptionMessage, ['exception' => $testException]); + + $autoloader = new Autoloader($this->createExceptionThrowingGeneratorTestDouble($testException)); + $autoloader->load(OneNonExistingClassName::class); + $autoloader->load(AnotherNonExistingClassName::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php index aab94361f752c..674335d361ffb 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/ValidateTest.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ImportExport\Controller\Adminhtml\Import; use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\HTTP\Adapter\FileTransferFactory; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; @@ -61,10 +63,7 @@ public function testValidationReturn(string $fileName, string $mimeType, string $this->_objectManager->configure( [ - 'preferences' => [ - \Magento\Framework\HTTP\Adapter\FileTransferFactory::class => - \Magento\ImportExport\Controller\Adminhtml\Import\HttpFactoryMock::class - ] + 'preferences' => [FileTransferFactory::class => HttpFactoryMock::class] ] ); @@ -81,7 +80,7 @@ public function testValidationReturn(string $fileName, string $mimeType, string /** * @return array */ - public function validationDataProvider() + public function validationDataProvider(): array { return [ [ diff --git a/dev/tests/integration/testsuite/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheStateTest.php b/dev/tests/integration/testsuite/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheStateTest.php new file mode 100644 index 0000000000000..456e6df3a7421 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheStateTest.php @@ -0,0 +1,69 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Observer\SwitchPageCacheOnMaintenance; + +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Page Cache state test. + */ +class PageCacheStateTest extends TestCase +{ + /** + * @var PageCacheState + */ + private $pageCacheStateStorage; + + /** + * @inheritdoc + */ + protected function setUp() + { + $objectManager = Bootstrap::getObjectManager(); + $this->pageCacheStateStorage = $objectManager->get(PageCacheState::class); + } + + /** + * Tests save state. + * + * @param bool $state + * @return void + * @dataProvider saveStateProvider + */ + public function testSave(bool $state) + { + $this->pageCacheStateStorage->save($state); + $this->assertEquals($state, $this->pageCacheStateStorage->isEnabled()); + } + + /** + * Tests flush state. + * + * @return void + */ + public function testFlush() + { + $this->pageCacheStateStorage->save(true); + $this->assertTrue($this->pageCacheStateStorage->isEnabled()); + $this->pageCacheStateStorage->flush(); + $this->assertFalse($this->pageCacheStateStorage->isEnabled()); + } + + /** + * Save state provider. + * + * @return array + */ + public function saveStateProvider(): array + { + return [[true], [false]]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index 802358c2c83c7..3a516befc37ff 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -131,6 +131,9 @@ public function testUpdateCustomerData() $this->assertContains($item, $actual); } $this->assertEquals('test@example.com', $quote->getCustomerEmail()); + $this->assertEquals('Joe', $quote->getCustomerFirstname()); + $this->assertEquals('Dou', $quote->getCustomerLastname()); + $this->assertEquals('Ivan', $quote->getCustomerMiddlename()); } /** diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php index ce3f3a3e1fc8e..cc051c11aabf8 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php @@ -5,7 +5,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; @@ -23,7 +22,10 @@ use PHPUnit\Framework\MockObject\MockObject; /** + * Class for test Account + * * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AccountTest extends \PHPUnit\Framework\TestCase { @@ -43,23 +45,33 @@ class AccountTest extends \PHPUnit\Framework\TestCase private $session; /** - * @magentoDataFixture Magento/Sales/_files/quote.php + * @inheritdoc */ protected function setUp() { $this->objectManager = Bootstrap::getObjectManager(); - $quote = $this->objectManager->create(Quote::class)->load(1); + parent::setUp(); + } + + /** + * Test for get form with existing customer + * + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testGetFormWithCustomer() + { + $customerGroup = 2; + $quote = $this->objectManager->create(Quote::class); $this->session = $this->getMockBuilder(SessionQuote::class) ->disableOriginalConstructor() - ->setMethods(['getCustomerId', 'getStore', 'getStoreId', 'getQuote', 'getQuoteId']) + ->setMethods(['getCustomerId','getQuote']) ->getMock(); - $this->session->method('getCustomerId') - ->willReturn(1); $this->session->method('getQuote') ->willReturn($quote); - $this->session->method('getQuoteId') - ->willReturn($quote->getId()); + $this->session->method('getCustomerId') + ->willReturn(1); + /** @var LayoutInterface $layout */ $layout = $this->objectManager->get(LayoutInterface::class); $this->accountBlock = $layout->createBlock( @@ -67,18 +79,20 @@ protected function setUp() 'address_block' . rand(), ['sessionQuote' => $this->session] ); - parent::setUp(); - } - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - */ - public function testGetForm() - { + $fixtureCustomerId = 1; + /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->objectManager->get(\Magento\Customer\Api\CustomerRepositoryInterface::class); + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $customerRepository->getById($fixtureCustomerId); + $customer->setGroupId($customerGroup); + $customerRepository->save($customer); + $expectedFields = ['group_id', 'email']; $form = $this->accountBlock->getForm(); self::assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); $fieldset = $form->getElements()[0]; + $content = $form->toHtml(); self::assertEquals(count($expectedFields), $fieldset->getElements()->count()); @@ -88,22 +102,45 @@ public function testGetForm() sprintf('Unexpected field "%s" in form.', $element->getId()) ); } + + self::assertContains( + '<option value="'.$customerGroup.'" selected="selected">Wholesale</option>', + $content, + 'The Customer Group specified for the chosen customer should be selected.' + ); + + self::assertContains( + 'value="'.$customer->getEmail().'"', + $content, + 'The Customer Email specified for the chosen customer should be input ' + ); } /** * Tests a case when user defined custom attribute has default value. * - * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoConfigFixture current_store customer/create_account/default_group 3 + * @magentoDataFixture Magento/Store/_files/core_second_third_fixturestore.php + * @magentoConfigFixture current_store customer/create_account/default_group 2 + * @magentoConfigFixture secondstore_store customer/create_account/default_group 3 */ public function testGetFormWithUserDefinedAttribute() { + /** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ + $storeManager = Bootstrap::getObjectManager()->get(\Magento\Store\Model\StoreManagerInterface::class); + $secondStore = $storeManager->getStore('secondstore'); + + $quoteSession = $this->objectManager->get(SessionQuote::class); + $quoteSession->setStoreId($secondStore->getId()); + $formFactory = $this->getFormFactoryMock(); $this->objectManager->addSharedInstance($formFactory, FormFactory::class); /** @var LayoutInterface $layout */ $layout = $this->objectManager->get(LayoutInterface::class); - $accountBlock = $layout->createBlock(Account::class, 'address_block' . rand()); + $accountBlock = $layout->createBlock( + Account::class, + 'address_block' . rand() + ); $form = $accountBlock->getForm(); $form->setUseContainer(true); @@ -116,7 +153,7 @@ public function testGetFormWithUserDefinedAttribute() ); self::assertContains( - '<option value="3" selected="selected">Customer Group 1</option>', + '<option value="3" selected="selected">Retailer</option>', $content, 'The Customer Group specified for the chosen store should be selected.' ); @@ -162,13 +199,13 @@ private function createCustomerGroupAttribute(): AttributeMetadataInterface { /** @var Option $option1 */ $option1 = $this->objectManager->create(Option::class); - $option1->setValue(3); - $option1->setLabel('Customer Group 1'); + $option1->setValue(2); + $option1->setLabel('Wholesale'); /** @var Option $option2 */ $option2 = $this->objectManager->create(Option::class); - $option2->setValue(4); - $option2->setLabel('Customer Group 2'); + $option2->setValue(3); + $option2->setLabel('Retailer'); /** @var AttributeMetadataInterfaceFactory $attributeMetadataFactory */ $attributeMetadataFactory = $this->objectManager->create(AttributeMetadataInterfaceFactory::class); diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php new file mode 100644 index 0000000000000..b6055f14e79d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Model/UrlFinderInterfaceTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\UrlRewrite\Model; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * @magentoDataFixture Magento/UrlRewrite/_files/url_rewrites.php + */ +class UrlFinderInterfaceTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var UrlFinderInterface + */ + private $urlFinder; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->urlFinder = Bootstrap::getObjectManager()->create(UrlFinderInterface::class); + } + + /** + * @dataProvider findOneDataProvider + * @param string $requestPath + * @param string $targetPath + * @param int $redirectType + */ + public function testFindOneByData(string $requestPath, string $targetPath, int $redirectType) + { + $data = [ + UrlRewrite::REQUEST_PATH => $requestPath, + ]; + $urlRewrite = $this->urlFinder->findOneByData($data); + $this->assertEquals($targetPath, $urlRewrite->getTargetPath()); + $this->assertEquals($redirectType, $urlRewrite->getRedirectType()); + } + + /** + * @return array + */ + public function findOneDataProvider(): array + { + return [ + ['string', 'test_page1', 0], + ['string/', 'string', 301], + ['string_permanent', 'test_page1', 301], + ['string_permanent/', 'test_page1', 301], + ['string_temporary', 'test_page1', 302], + ['string_temporary/', 'test_page1', 302], + ['строка', 'test_page1', 0], + ['строка/', 'строка', 301], + [urlencode('строка'), 'test_page2', 0], + [urlencode('строка') . '/', urlencode('строка'), 301], + ['другая_строка', 'test_page1', 302], + ['другая_строка/', 'test_page1', 302], + [urlencode('другая_строка'), 'test_page1', 302], + [urlencode('другая_строка') . '/', 'test_page1', 302], + ['السلسلة', 'test_page1', 0], + [urlencode('السلسلة'), 'test_page1', 0], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php new file mode 100644 index 0000000000000..9edc6507308ee --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$rewritesData = [ + [ + 'string', 'test_page1', 0 + ], + [ + 'string_permanent', 'test_page1', \Magento\UrlRewrite\Model\OptionProvider::PERMANENT + ], + [ + 'string_temporary', 'test_page1', \Magento\UrlRewrite\Model\OptionProvider::TEMPORARY + ], + [ + 'строка', 'test_page1', 0 + ], + [ + urlencode('строка'), 'test_page2', 0 + ], + [ + 'другая_строка', 'test_page1', \Magento\UrlRewrite\Model\OptionProvider::TEMPORARY + ], + [ + 'السلسلة', 'test_page1', 0 + ], +]; + +$rewriteResource = $objectManager->create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewrite::class); +foreach ($rewritesData as $rewriteData) { + list ($requestPath, $targetPath, $redirectType) = $rewriteData; + $rewrite = $objectManager->create(\Magento\UrlRewrite\Model\UrlRewrite::class); + $rewrite->setEntityType('custom') + ->setRequestPath($requestPath) + ->setTargetPath($targetPath) + ->setRedirectType($redirectType); + $rewriteResource->save($rewrite); +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php new file mode 100644 index 0000000000000..a98f947d614e0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrites_rollback.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$urlRewriteCollection = $objectManager->create(\Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection::class); +$collection = $urlRewriteCollection + ->addFieldToFilter('target_path', ['test_page1', 'test_page2']) + ->load() + ->walk('delete'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php new file mode 100644 index 0000000000000..47705262caaf3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/ShareTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Controller; + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Area; +use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Message\MessageInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * @magentoAppIsolation enabled + */ +class ShareTest extends AbstractController +{ + /** + * Test share wishlist with correct data + * + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testSuccessfullyShareWishlist() + { + $this->login(1); + $this->prepareRequestData(); + $this->dispatch('wishlist/index/send/'); + + $this->assertSessionMessages( + $this->equalTo(['Your wish list has been shared.']), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Test share wishlist with incorrect data + * + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testShareWishlistWithoutEmails() + { + $this->login(1); + $this->prepareRequestData(true); + $this->dispatch('wishlist/index/send/'); + + $this->assertSessionMessages( + $this->equalTo(['Please enter an email address.']), + MessageInterface::TYPE_ERROR + ); + } + + /** + * Login the user + * + * @param string $customerId Customer to mark as logged in for the session + * @return void + */ + protected function login($customerId) + { + /** @var Session $session */ + $session = $this->_objectManager->get(Session::class); + $session->loginById($customerId); + } + + /** + * Prepares the request with data + * + * @param bool $invalidData + * @return void + */ + private function prepareRequestData($invalidData = false) + { + Bootstrap::getInstance()->loadArea(Area::AREA_FRONTEND); + $emails = !$invalidData ? 'email-1@example.com,email-2@example.com' : ''; + + /** @var FormKey $formKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'emails' => $emails, + 'message' => '', + 'form_key' => $formKey->getFormKey(), + ]; + + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setPostValue($post); + } +} diff --git a/dev/tests/static/framework/Magento/ruleset.xml b/dev/tests/static/framework/Magento/ruleset.xml index 56a5a9e55c30e..fac0842214d92 100644 --- a/dev/tests/static/framework/Magento/ruleset.xml +++ b/dev/tests/static/framework/Magento/ruleset.xml @@ -25,5 +25,6 @@ <rule ref="Squiz.Commenting.DocCommentAlignment"/> <rule ref="Squiz.Functions.GlobalFunction"/> + <rule ref="Squiz.Operators.ValidLogicalOperators"/> <rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/> </ruleset> diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php index 5c1e342e1bc81..4b31b80f1d7b3 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php @@ -191,7 +191,7 @@ private function assertClassesExist($classes, $path) foreach ($classes as $class) { $class = trim($class, '\\'); try { - if (strrchr($class, '\\') === false and !Classes::isVirtual($class)) { + if (strrchr($class, '\\') === false && !Classes::isVirtual($class)) { $badUsages[] = $class; continue; } else { diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php index 6fc84486c626b..2f1ab7a75bc83 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ComposerTest.php @@ -452,7 +452,7 @@ private function convertModuleToPackageName($moduleName) { list($vendor, $name) = explode('_', $moduleName, 2); $package = 'module'; - foreach (preg_split('/([A-Z][a-z\d]+)/', $name, -1, PREG_SPLIT_DELIM_CAPTURE) as $chunk) { + foreach (preg_split('/([A-Z\d][a-z]*)/', $name, -1, PREG_SPLIT_DELIM_CAPTURE) as $chunk) { $package .= $chunk ? "-{$chunk}" : ''; } return strtolower("{$vendor}/{$package}"); diff --git a/lib/internal/Magento/Framework/App/MaintenanceMode.php b/lib/internal/Magento/Framework/App/MaintenanceMode.php index 4e4328cb72aef..225375c7df463 100644 --- a/lib/internal/Magento/Framework/App/MaintenanceMode.php +++ b/lib/internal/Magento/Framework/App/MaintenanceMode.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Event\Manager; /** * Application Maintenance Mode @@ -39,13 +40,18 @@ class MaintenanceMode protected $flagDir; /** - * Constructor - * + * @var Manager + */ + private $eventManager; + + /** * @param \Magento\Framework\Filesystem $filesystem + * @param Manager|null $eventManager */ - public function __construct(Filesystem $filesystem) + public function __construct(Filesystem $filesystem, Manager $eventManager = null) { $this->flagDir = $filesystem->getDirectoryWrite(self::FLAG_DIR); + $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(Manager::class); } /** @@ -73,6 +79,8 @@ public function isOn($remoteAddr = '') */ public function set($isOn) { + $this->eventManager->dispatch('maintenance_mode_changed', ['isOn' => $isOn]); + if ($isOn) { return $this->flagDir->touch(self::FLAG_FILENAME); } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php b/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php index 5d1c22a38af4d..09bcbd760c87a 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/MaintenanceModeTest.php @@ -6,9 +6,17 @@ namespace Magento\Framework\App\Test\Unit; -use \Magento\Framework\App\MaintenanceMode; +use Magento\Framework\App\MaintenanceMode; +use Magento\Framework\Event\Manager; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Filesystem; +use PHPUnit\Framework\TestCase; -class MaintenanceModeTest extends \PHPUnit\Framework\TestCase +/** + * MaintenanceMode Test + */ +class MaintenanceModeTest extends TestCase { /** * @var MaintenanceMode @@ -16,141 +24,213 @@ class MaintenanceModeTest extends \PHPUnit\Framework\TestCase protected $model; /** - * @var \Magento\Framework\Filesystem\Directory\WriteInterface | \PHPUnit_Framework_MockObject_MockObject + * @var WriteInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $flagDir; + /** + * @var Manager|\PHPUnit_Framework_MockObject_MockObject + */ + private $eventManager; + + /** + * @inheritdoc + */ protected function setup() { - $this->flagDir = $this->getMockForAbstractClass(\Magento\Framework\Filesystem\Directory\WriteInterface::class); - $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); - $filesystem->expects($this->any()) - ->method('getDirectoryWrite') - ->will($this->returnValue($this->flagDir)); + $this->flagDir = $this->getMockForAbstractClass(WriteInterface::class); + $filesystem = $this->createMock(Filesystem::class); + $filesystem->method('getDirectoryWrite') + ->willReturn($this->flagDir); + $this->eventManager = $this->createMock(Manager::class); - $this->model = new MaintenanceMode($filesystem); + $objectManager = new ObjectManager($this); + $this->model = $objectManager->getObject(MaintenanceMode::class, [ + 'filesystem' => $filesystem, + 'eventManager' => $this->eventManager, + ]); } + /** + * Is On initial test + * + * @return void + */ public function testIsOnInitial() { - $this->flagDir->expects($this->once())->method('isExist') + $this->flagDir->expects($this->once()) + ->method('isExist') ->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); + ->willReturn(false); $this->assertFalse($this->model->isOn()); } + /** + * Is On without ip test + * + * @return void + */ public function testisOnWithoutIP() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, false], ]; - $this->flagDir->expects($this->exactly(2))->method('isExist') - ->will(($this->returnValueMap($mapisExist))); + $this->flagDir->expects($this->exactly(2)) + ->method('isExist') + ->willReturnMap($mapisExist); $this->assertTrue($this->model->isOn()); } + /** + * Is On with IP test + * + * @return void + */ public function testisOnWithIP() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->exactly(2))->method('isExist') - ->will(($this->returnValueMap($mapisExist))); + $this->flagDir->expects($this->exactly(2)) + ->method('isExist') + ->willReturnMap($mapisExist); $this->assertFalse($this->model->isOn()); } + /** + * Is On with IP but no Maintenance files test + * + * @return void + */ public function testisOnWithIPNoMaintenance() { - $this->flagDir->expects($this->once())->method('isExist') + $this->flagDir->expects($this->once()) + ->method('isExist') ->with(MaintenanceMode::FLAG_FILENAME) ->willReturn(false); $this->assertFalse($this->model->isOn()); } + /** + * Maintenance Mode On test + * + * Tests common scenario with Full Page Cache is set to On + * + * @return void + */ public function testMaintenanceModeOn() { - $this->flagDir->expects($this->at(0))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - $this->flagDir->expects($this->at(1))->method('touch')->will($this->returnValue(true)); - $this->flagDir->expects($this->at(2))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(true)); - $this->flagDir->expects($this->at(3))->method('isExist')->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue(false)); + $this->eventManager->expects($this->once()) + ->method('dispatch') + ->with('maintenance_mode_changed', ['isOn' => true]); - $this->assertFalse($this->model->isOn()); - $this->assertTrue($this->model->set(true)); - $this->assertTrue($this->model->isOn()); + $this->flagDir->expects($this->once()) + ->method('touch') + ->with(MaintenanceMode::FLAG_FILENAME); + + $this->model->set(true); } + /** + * Maintenance Mode Off test + * + * Tests common scenario when before Maintenance Mode Full Page Cache was setted to on + * + * @return void + */ public function testMaintenanceModeOff() { - $this->flagDir->expects($this->at(0))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(true)); - $this->flagDir->expects($this->at(1))->method('delete')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - $this->flagDir->expects($this->at(2))->method('isExist')->with(MaintenanceMode::FLAG_FILENAME) - ->will($this->returnValue(false)); - - $this->assertFalse($this->model->set(false)); - $this->assertFalse($this->model->isOn()); + $this->eventManager->expects($this->once()) + ->method('dispatch') + ->with('maintenance_mode_changed', ['isOn' => false]); + + $this->flagDir->method('isExist') + ->with(MaintenanceMode::FLAG_FILENAME) + ->willReturn(true); + + $this->flagDir->expects($this->once()) + ->method('delete') + ->with(MaintenanceMode::FLAG_FILENAME); + + $this->model->set(false); } + /** + * Set empty addresses test + * + * @return void + */ public function testSetAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('writeFile') + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('writeFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue(true)); + ->willReturn(true); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('')); + ->willReturn(''); $this->model->setAddresses(''); $this->assertEquals([''], $this->model->getAddressInfo()); } + /** + * Set single address test + * + * @return void + */ public function testSetSingleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('writeFile') - ->will($this->returnValue(10)); + $this->flagDir->method('writeFile') + ->willReturn(10); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1')); + ->willReturn('address1'); $this->model->setAddresses('address1'); $this->assertEquals(['address1'], $this->model->getAddressInfo()); } + /** + * Is On when multiple addresses test was setted + * + * @return void + */ public function testOnSetMultipleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, true], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('writeFile') - ->will($this->returnValue(10)); + $this->flagDir->method('writeFile') + ->willReturn(10); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1,10.50.60.123')); + ->willReturn('address1,10.50.60.123'); $expectedArray = ['address1', '10.50.60.123']; $this->model->setAddresses('address1,10.50.60.123'); @@ -159,18 +239,25 @@ public function testOnSetMultipleAddresses() $this->assertTrue($this->model->isOn('address3')); } + /** + * Is Off when multiple addresses test was setted + * + * @return void + */ public function testOffSetMultipleAddresses() { $mapisExist = [ [MaintenanceMode::FLAG_FILENAME, false], [MaintenanceMode::IP_FILENAME, true], ]; - $this->flagDir->expects($this->any())->method('isExist')->will($this->returnValueMap($mapisExist)); - $this->flagDir->expects($this->any())->method('delete')->will($this->returnValueMap($mapisExist)); + $this->flagDir->method('isExist') + ->willReturnMap($mapisExist); + $this->flagDir->method('delete') + ->willReturnMap($mapisExist); - $this->flagDir->expects($this->any())->method('readFile') + $this->flagDir->method('readFile') ->with(MaintenanceMode::IP_FILENAME) - ->will($this->returnValue('address1,10.50.60.123')); + ->willReturn('address1,10.50.60.123'); $expectedArray = ['address1', '10.50.60.123']; $this->model->setAddresses('address1,10.50.60.123'); diff --git a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php index c214008393609..f8e469fe05265 100644 --- a/lib/internal/Magento/Framework/Code/Generator/Autoloader.php +++ b/lib/internal/Magento/Framework/Code/Generator/Autoloader.php @@ -3,37 +3,94 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Code\Generator; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Code\Generator; +use Psr\Log\LoggerInterface; +/** + * Class loader and generator. + */ class Autoloader { /** - * @var \Magento\Framework\Code\Generator + * @var Generator */ protected $_generator; /** - * @param \Magento\Framework\Code\Generator $generator + * Enables guarding against spamming the debug log with duplicate messages, as + * the generation exception will be thrown multiple times within a single request. + * + * @var string + */ + private $lastGenerationErrorMessage; + + /** + * @param Generator $generator */ - public function __construct( - \Magento\Framework\Code\Generator $generator - ) { + public function __construct(Generator $generator) + { $this->_generator = $generator; } /** * Load specified class name and generate it if necessary * + * According to PSR-4 section 2.4 an autoloader MUST NOT throw an exception and SHOULD NOT return a value. + * + * @see https://www.php-fig.org/psr/psr-4/ + * * @param string $className - * @return bool True if class was loaded + * @return void */ public function load($className) { - if (!class_exists($className)) { - return Generator::GENERATION_ERROR != $this->_generator->generateClass($className); + if (! class_exists($className)) { + try { + $this->_generator->generateClass($className); + } catch (\Exception $exception) { + $this->tryToLogExceptionMessageIfNotDuplicate($exception); + } + } + } + + /** + * Log exception. + * + * @param \Exception $exception + */ + private function tryToLogExceptionMessageIfNotDuplicate(\Exception $exception) + { + if ($this->lastGenerationErrorMessage !== $exception->getMessage()) { + $this->lastGenerationErrorMessage = $exception->getMessage(); + $this->tryToLogException($exception); + } + } + + /** + * Try to capture the exception message. + * + * The Autoloader is instantiated before the ObjectManager, so the LoggerInterface can not be injected. + * The Logger is instantiated in the try/catch block because ObjectManager might still not be initialized. + * In that case the exception message can not be captured. + * + * The debug level is used for logging in case class generation fails for a common class, but a custom + * autoloader is used later in the stack. A more severe log level would fill the logs with messages on production. + * The exception message now can be accessed in developer mode if debug logging is enabled. + * + * @param \Exception $exception + * @return void + */ + private function tryToLogException(\Exception $exception) + { + try { + $logger = ObjectManager::getInstance()->get(LoggerInterface::class); + $logger->debug($exception->getMessage(), ['exception' => $exception]); + } catch (\Exception $ignoreThisException) { + // Do not take an action here, since the original exception might have been caused by logger } - return true; } } diff --git a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php index 3ce78177d875f..6961426fcc7f1 100644 --- a/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php +++ b/lib/internal/Magento/Framework/DB/Sql/UnionExpression.php @@ -22,18 +22,25 @@ class UnionExpression extends Expression */ protected $type; + /** + * @var string + */ + private $pattern; + /** * @param Select[] $parts - * @param string $type + * @param string $type (optional) + * @param string $pattern (optional) */ - public function __construct(array $parts, $type = Select::SQL_UNION) + public function __construct(array $parts, $type = Select::SQL_UNION, $pattern = '') { $this->parts = $parts; $this->type = $type; + $this->pattern = $pattern; } /** - * @return string + * @inheritdoc */ public function __toString() { @@ -45,6 +52,11 @@ public function __toString() $parts[] = $part; } } - return implode($parts, $this->type); + $sql = implode($parts, $this->type); + if ($this->pattern) { + return sprintf($this->pattern, $sql); + } + + return $sql; } } diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/Sql/UnionExpressionTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/Sql/UnionExpressionTest.php index 1a3a91ea6bd54..a5aca9707081b 100644 --- a/lib/internal/Magento/Framework/DB/Test/Unit/Sql/UnionExpressionTest.php +++ b/lib/internal/Magento/Framework/DB/Test/Unit/Sql/UnionExpressionTest.php @@ -6,11 +6,22 @@ namespace Magento\Framework\DB\Test\Unit\Sql; use Magento\Framework\DB\Select; +use Magento\Framework\DB\Sql\UnionExpression; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; class UnionExpressionTest extends \PHPUnit\Framework\TestCase { - public function testToString() + /** + * @param string $type + * @param string $pattern + * @param string $expectedSql + * @return void + * @dataProvider toStringDataProvider + */ + public function testToString(string $type, string $pattern, string $expectedSql) { + $objectManager = new ObjectManager($this); + $sqlMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class) ->disableOriginalConstructor() ->getMock(); @@ -21,7 +32,34 @@ public function testToString() $sqlMock, '(test_column)' ]; - $model = new \Magento\Framework\DB\Sql\UnionExpression($parts); - $this->assertEquals('(test_assemble)' . Select::SQL_UNION . '(test_column)', $model->__toString()); + $model = $objectManager->getObject( + UnionExpression::class, + [ + 'parts' => $parts, + 'type' => $type, + 'pattern' => $pattern, + ] + ); + + $this->assertEquals($expectedSql, $model->__toString()); + } + + /** + * @return array + */ + public function toStringDataProvider(): array + { + return [ + [ + 'type' => Select::SQL_UNION, + 'pattern' => '', + 'expectedSql' => "(test_assemble)" . Select::SQL_UNION . "(test_column)", + ], + [ + 'type' => Select::SQL_UNION, + 'pattern' => 'test_with_pattern %s', + 'expectedSql' => "test_with_pattern (test_assemble)" . Select::SQL_UNION . "(test_column)", + ], + ]; } } diff --git a/lib/internal/Magento/Framework/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/Filesystem/DirectoryList.php index f07932d45ecb6..ef27cc19c4697 100644 --- a/lib/internal/Magento/Framework/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/Filesystem/DirectoryList.php @@ -8,6 +8,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Filesystem; /** @@ -96,7 +97,8 @@ public function __construct($root, array $config = []) static::validate($config); $this->root = $this->normalizePath($root); $this->directories = static::getDefaultConfig(); - $this->directories[self::SYS_TMP] = [self::PATH => realpath(sys_get_temp_dir())]; + $sysTmpPath = get_cfg_var('upload_tmp_dir') ?: sys_get_temp_dir(); + $this->directories[self::SYS_TMP] = [self::PATH => realpath($sysTmpPath)]; // inject custom values from constructor foreach ($this->directories as $code => $dir) { diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/Http.php b/lib/internal/Magento/Framework/Filesystem/Driver/Http.php index 5c7fdb0630186..46ad773cff201 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/Http.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/Http.php @@ -27,26 +27,18 @@ class Http extends File * * @param string $path * @return bool - * @throws FileSystemException */ public function isExists($path) { $headers = array_change_key_case(get_headers($this->getScheme() . $path, 1), CASE_LOWER); - $status = $headers[0]; - /* Handling 302 redirection */ - if (strpos($status, '302 Found') !== false && isset($headers[1])) { + /* Handling 301 or 302 redirection */ + if (isset($headers[1]) && preg_match('/30[12]/', $status)) { $status = $headers[1]; } - if (strpos($status, '200 OK') === false) { - $result = false; - } else { - $result = true; - } - - return $result; + return !(strpos($status, '200 OK') === false); } /** diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index 40799bfe0a6b5..a2e772853efcb 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -227,7 +227,7 @@ public function templateDirective($construction) { // Processing of {template config_path=... [...]} statement $templateParameters = $this->getParameters($construction[2]); - if (!isset($templateParameters['config_path']) or !$this->getTemplateProcessor()) { + if (!isset($templateParameters['config_path']) || !$this->getTemplateProcessor()) { // Not specified template or not set include processor $replacedValue = '{Error in template processing}'; } else { diff --git a/lib/internal/Magento/Framework/Mail/Message.php b/lib/internal/Magento/Framework/Mail/Message.php index c8f0e75c52800..96566ad82b8eb 100644 --- a/lib/internal/Magento/Framework/Mail/Message.php +++ b/lib/internal/Magento/Framework/Mail/Message.php @@ -89,10 +89,23 @@ public function getBody() /** * @inheritdoc + * + * @deprecated This function is missing the from name. The + * setFromAddress() function sets both from address and from name. + * @see setFromAddress() */ public function setFrom($fromAddress) { - $this->zendMessage->setFrom($fromAddress); + $this->setFromAddress($fromAddress, null); + return $this; + } + + /** + * @inheritdoc + */ + public function setFromAddress($fromAddress, $fromName = null) + { + $this->zendMessage->setFrom($fromAddress, $fromName); return $this; } diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php index 5e54a9441d1a1..b5be1cbf52cfd 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php @@ -48,13 +48,6 @@ class TransportBuilder */ protected $templateOptions; - /** - * Mail from address - * - * @var string|array - */ - private $from; - /** * Mail Transport * @@ -180,12 +173,29 @@ public function setReplyTo($email, $name = null) /** * Set mail from address * + * @deprecated This function sets the from address but does not provide + * a way of setting the correct from addresses based on the scope. + * @see setFromByScope() + * * @param string|array $from * @return $this */ public function setFrom($from) { - $this->from = $from; + return $this->setFromByScope($from, null); + } + + /** + * Set mail from address by Scope + * + * @param string|array $from + * @param string|int $scopeId + * @return $this + */ + public function setFromByScope($from, $scopeId = null) + { + $result = $this->_senderResolver->resolve($from, $scopeId); + $this->message->setFromAddress($result['email'], $result['name']); return $this; } @@ -262,7 +272,6 @@ protected function reset() $this->templateIdentifier = null; $this->templateVars = null; $this->templateOptions = null; - $this->from = null; return $this; } @@ -296,14 +305,6 @@ protected function prepareMessage() ->setBody($body) ->setSubject(html_entity_decode($template->getSubject(), ENT_QUOTES)); - if ($this->from) { - $from = $this->_senderResolver->resolve( - $this->from, - $template->getDesignConfig()->getStore() - ); - $this->message->setFrom($from['email'], $from['name']); - } - return $this; } } diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php index 785c93824a57d..bdb7619d7fe07 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php @@ -8,6 +8,13 @@ use Magento\Framework\Mail\MessageInterface; +/** + * Class TransportBuilderByStore + * + * @deprecated The ability to set From address based on store is now available + * in the \Magento\Framework\Mail\Template\TransportBuilder class + * @see \Magento\Framework\Mail\Template\TransportBuilder::setFromByScope + */ class TransportBuilderByStore { /** diff --git a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php index 0bbfb37356caa..6c5c48fba1b9b 100644 --- a/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php +++ b/lib/internal/Magento/Framework/Mail/Test/Unit/Template/TransportBuilderTest.php @@ -7,7 +7,6 @@ namespace Magento\Framework\Mail\Test\Unit\Template; use Magento\Framework\App\TemplateTypesInterface; -use Magento\Framework\DataObject; use Magento\Framework\Mail\MessageInterface; /** @@ -100,37 +99,17 @@ protected function setUp() */ public function testGetTransport($templateType, $messageType, $bodyText, $templateNamespace) { + $this->builder->setTemplateModel($templateNamespace); + $vars = ['reason' => 'Reason', 'customer' => 'Customer']; $options = ['area' => 'frontend', 'store' => 1]; - $from = 'email_from'; - $sender = ['email' => 'from@example.com', 'name' => 'name']; - - $this->builder->setTemplateModel($templateNamespace); - $this->builder->setFrom($from); - $template = $this->createPartialMock( - \Magento\Framework\Mail\TemplateInterface::class, - [ - 'setVars', - 'isPlain', - 'setOptions', - 'getSubject', - 'getType', - 'processTemplate', - 'getDesignConfig', - ] - ); + $template = $this->createMock(\Magento\Framework\Mail\TemplateInterface::class); $template->expects($this->once())->method('setVars')->with($this->equalTo($vars))->willReturnSelf(); $template->expects($this->once())->method('setOptions')->with($this->equalTo($options))->willReturnSelf(); $template->expects($this->once())->method('getSubject')->willReturn('Email Subject'); $template->expects($this->once())->method('getType')->willReturn($templateType); $template->expects($this->once())->method('processTemplate')->willReturn($bodyText); - $template->method('getDesignConfig')->willReturn(new DataObject($options)); - - $this->senderResolverMock->expects($this->once()) - ->method('resolve') - ->with($from, 1) - ->willReturn($sender); $this->templateFactoryMock->expects($this->once()) ->method('get') @@ -149,9 +128,6 @@ public function testGetTransport($templateType, $messageType, $bodyText, $templa ->method('setBody') ->with($this->equalTo($bodyText)) ->willReturnSelf(); - $this->messageMock->method('setFrom') - ->with($sender['email'], $sender['name']) - ->willReturnSelf(); $transport = $this->createMock(\Magento\Framework\Mail\TransportInterface::class); @@ -184,6 +160,25 @@ public function getTransportDataProvider() ] ]; } + + /** + * @return void + */ + public function testSetFromByScope() + { + $sender = ['email' => 'from@example.com', 'name' => 'name']; + $scopeId = 1; + $this->senderResolverMock->expects($this->once()) + ->method('resolve') + ->with($sender, $scopeId) + ->willReturn($sender); + $this->messageMock->expects($this->once()) + ->method('setFromAddress') + ->with('from@example.com', 'name') + ->willReturnSelf(); + + $this->builder->setFromByScope($sender, $scopeId); + } /** * @return void diff --git a/lib/internal/Magento/Framework/Message/Manager.php b/lib/internal/Magento/Framework/Message/Manager.php index f31892a938fb1..4e73b6112f9d8 100644 --- a/lib/internal/Magento/Framework/Message/Manager.php +++ b/lib/internal/Magento/Framework/Message/Manager.php @@ -11,6 +11,8 @@ /** * Message manager model + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Manager implements ManagerInterface @@ -226,7 +228,7 @@ public function addUniqueMessages(array $messages, $group = null) $items = $this->getMessages(false, $group)->getItems(); foreach ($messages as $message) { - if ($message instanceof MessageInterface and !in_array($message, $items, false)) { + if ($message instanceof MessageInterface && !in_array($message, $items, false)) { $this->addMessage($message, $group); } } diff --git a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php index bdfb77762b41c..72421f793f131 100644 --- a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php +++ b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php @@ -126,16 +126,21 @@ private function getModuleConfigs() * * @param array $origList * @return array - * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @throws \Exception */ - private function sortBySequence($origList) + private function sortBySequence(array $origList): array { ksort($origList); + $modules = $this->prearrangeModules($origList); + $expanded = []; - foreach ($origList as $moduleName => $value) { + foreach (array_keys($modules) as $moduleName) { + $sequence = $this->expandSequence($origList, $moduleName); + asort($sequence); + $expanded[] = [ 'name' => $moduleName, - 'sequence' => $this->expandSequence($origList, $moduleName), + 'sequence' => $sequence, ]; } @@ -143,7 +148,7 @@ private function sortBySequence($origList) $total = count($expanded); for ($i = 0; $i < $total - 1; $i++) { for ($j = $i; $j < $total; $j++) { - if (in_array($expanded[$j]['name'], $expanded[$i]['sequence'])) { + if (in_array($expanded[$j]['name'], $expanded[$i]['sequence'], true)) { $temp = $expanded[$i]; $expanded[$i] = $expanded[$j]; $expanded[$j] = $temp; @@ -159,6 +164,27 @@ private function sortBySequence($origList) return $result; } + /** + * Prearrange all modules by putting those from Magento before the others + * + * @param array $modules + * @return array + */ + private function prearrangeModules(array $modules): array + { + $breakdown = ['magento' => [], 'others' => []]; + + foreach ($modules as $moduleName => $moduleDetails) { + if (strpos($moduleName, 'Magento_') !== false) { + $breakdown['magento'][$moduleName] = $moduleDetails; + } else { + $breakdown['others'][$moduleName] = $moduleDetails; + } + } + + return array_merge($breakdown['magento'], $breakdown['others']); + } + /** * Accumulate information about all transitive "sequence" references * diff --git a/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php b/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php index fe613450fd485..827a0df8e4930 100644 --- a/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php +++ b/lib/internal/Magento/Framework/Module/Test/Unit/ModuleList/LoaderTest.php @@ -160,4 +160,55 @@ public function testLoadCircular() ])); $this->loader->load(); } + + /** + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testLoadPrearranged() + { + $fixtures = [ + 'Foo_Bar' => ['name' => 'Foo_Bar', 'sequence' => ['Magento_Store']], + 'Magento_Directory' => ['name' => 'Magento_Directory', 'sequence' => ['Magento_Store']], + 'Magento_Store' => ['name' => 'Magento_Store', 'sequence' => []], + 'Magento_Theme' => ['name' => 'Magento_Theme', 'sequence' => ['Magento_Store', 'Magento_Directory']], + 'Test_HelloWorld' => ['name' => 'Test_HelloWorld', 'sequence' => ['Magento_Theme']] + ]; + + $index = 0; + foreach ($fixtures as $name => $fixture) { + $this->converter->expects($this->at($index++))->method('convert')->willReturn([$name => $fixture]); + } + + $this->registry->expects($this->once()) + ->method('getPaths') + ->willReturn([ + '/path/to/Foo_Bar', + '/path/to/Magento_Directory', + '/path/to/Magento_Store', + '/path/to/Magento_Theme', + '/path/to/Test_HelloWorld' + ]); + + $this->driver->expects($this->exactly(5)) + ->method('fileGetContents') + ->will($this->returnValueMap([ + ['/path/to/Foo_Bar/etc/module.xml', null, null, self::$sampleXml], + ['/path/to/Magento_Directory/etc/module.xml', null, null, self::$sampleXml], + ['/path/to/Magento_Store/etc/module.xml', null, null, self::$sampleXml], + ['/path/to/Magento_Theme/etc/module.xml', null, null, self::$sampleXml], + ['/path/to/Test_HelloWorld/etc/module.xml', null, null, self::$sampleXml], + ])); + + // Load the full module list information + $result = $this->loader->load(); + + $this->assertSame( + ['Magento_Store', 'Magento_Directory', 'Magento_Theme', 'Foo_Bar', 'Test_HelloWorld'], + array_keys($result) + ); + + foreach ($fixtures as $name => $fixture) { + $this->assertSame($fixture, $result[$name]); + } + } } diff --git a/lib/internal/Magento/Framework/View/Context.php b/lib/internal/Magento/Framework/View/Context.php index 0c3932ffe4bd7..508d63d158bd7 100644 --- a/lib/internal/Magento/Framework/View/Context.php +++ b/lib/internal/Magento/Framework/View/Context.php @@ -14,6 +14,7 @@ use Magento\Framework\Event\ManagerInterface; use Psr\Log\LoggerInterface as Logger; use Magento\Framework\Session\SessionManager; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\TranslateInterface; use Magento\Framework\UrlInterface; use Magento\Framework\View\ConfigInterface as ViewConfig; @@ -144,6 +145,7 @@ class Context * @param Logger $logger * @param AppState $appState * @param LayoutInterface $layout + * @param SessionManagerInterface|null $sessionManager * * @todo reduce parameter number * @@ -163,7 +165,8 @@ public function __construct( CacheState $cacheState, Logger $logger, AppState $appState, - LayoutInterface $layout + LayoutInterface $layout, + SessionManagerInterface $sessionManager = null ) { $this->request = $request; $this->eventManager = $eventManager; @@ -171,7 +174,7 @@ public function __construct( $this->translator = $translator; $this->cache = $cache; $this->design = $design; - $this->session = $session; + $this->session = $sessionManager ?: $session; $this->scopeConfig = $scopeConfig; $this->frontController = $frontController; $this->viewConfig = $viewConfig; @@ -332,15 +335,13 @@ public function getModuleName() } /** - * Retrieve the module name - * - * @return string + * Get Front Name * - * @todo alias of getModuleName + * @see getModuleName */ public function getFrontName() { - return $this->getRequest()->getModuleName(); + return $this->getModuleName(); } /** diff --git a/lib/internal/Magento/Framework/View/Layout/etc/head.xsd b/lib/internal/Magento/Framework/View/Layout/etc/head.xsd index a913507ae17b3..15762dc2f0ae6 100644 --- a/lib/internal/Magento/Framework/View/Layout/etc/head.xsd +++ b/lib/internal/Magento/Framework/View/Layout/etc/head.xsd @@ -20,6 +20,15 @@ <xs:attribute name="type" type="xs:string"/> <xs:attribute name="order" type="xs:integer"/> <xs:attribute name="src_type" type="xs:string"/> + <xs:attribute name="as"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="font" /> + <xs:enumeration value="script" /> + <xs:enumeration value="style" /> + </xs:restriction> + </xs:simpleType> + </xs:attribute> </xs:complexType> <xs:complexType name="metaType"> diff --git a/lib/internal/Magento/Framework/View/Page/Config.php b/lib/internal/Magento/Framework/View/Page/Config.php index 226abc538112b..da7bcb128f4b8 100644 --- a/lib/internal/Magento/Framework/View/Page/Config.php +++ b/lib/internal/Magento/Framework/View/Page/Config.php @@ -498,7 +498,7 @@ public function addRss($title, $href) */ public function addBodyClass($className) { - $className = preg_replace('#[^a-z0-9]+#', '-', strtolower($className)); + $className = preg_replace('#[^a-z0-9-_]+#', '-', strtolower($className)); $bodyClasses = $this->getElementAttribute(self::ELEMENT_TYPE_BODY, self::BODY_ATTRIBUTE_CLASS); $bodyClasses = $bodyClasses ? explode(' ', $bodyClasses) : []; $bodyClasses[] = $className; diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/Generator/BodyTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/Generator/BodyTest.php index 0f59c302f943f..ed926afa00856 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/Generator/BodyTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/Generator/BodyTest.php @@ -57,13 +57,13 @@ public function testProcess() ->method('getPageConfigStructure') ->willReturn($structureMock); - $bodyClasses = ['class_1', 'class_2']; + $bodyClasses = ['class_1', 'class--2']; $structureMock->expects($this->once()) ->method('getBodyClasses') ->will($this->returnValue($bodyClasses)); $this->pageConfigMock->expects($this->exactly(2)) ->method('addBodyClass') - ->withConsecutive(['class_1'], ['class_2']); + ->withConsecutive(['class_1'], ['class--2']); $this->assertEquals( $this->bodyGenerator, diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/StructureTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/StructureTest.php index ed15a356cc4c7..d2eba5d2fa1b3 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/StructureTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Page/Config/StructureTest.php @@ -58,7 +58,7 @@ public function testSetElementAttribute() public function testSetBodyClass() { $class1 = 'class_1'; - $class2 = 'class_2'; + $class2 = 'class--2'; $expected = [$class1, $class2]; $this->structure->setBodyClass($class1); $this->structure->setBodyClass($class2); diff --git a/lib/web/mage/adminhtml/grid.js b/lib/web/mage/adminhtml/grid.js index c9af869d79161..5f7a709f04fea 100644 --- a/lib/web/mage/adminhtml/grid.js +++ b/lib/web/mage/adminhtml/grid.js @@ -530,13 +530,20 @@ define([ /** * @param {Object} event + * @param {*} lastId */ - inputPage: function (event) { + inputPage: function (event, lastId) { var element = Event.element(event), - keyCode = event.keyCode || event.which; + keyCode = event.keyCode || event.which, + enteredValue = parseInt(element.value, 10), + pageId = parseInt(lastId, 10); if (keyCode == Event.KEY_RETURN) { //eslint-disable-line eqeqeq - this.setPage(element.value); + if (enteredValue > pageId) { + this.setPage(pageId); + } else { + this.setPage(enteredValue); + } } /*if(keyCode>47 && keyCode<58){ diff --git a/lib/web/mage/adminhtml/tools.js b/lib/web/mage/adminhtml/tools.js index f23a0193cb6b2..ed4bab7102ae5 100644 --- a/lib/web/mage/adminhtml/tools.js +++ b/lib/web/mage/adminhtml/tools.js @@ -89,12 +89,6 @@ function checkByProductPriceType(elem) { } -Event.observe(window, 'load', function () { - if ($('price_default') && $('price_default').checked) { - $('price').disabled = 'disabled'; - } -}); - function toggleSeveralValueElements(checkbox, containers, excludedElements, checked) { 'use strict'; diff --git a/lib/web/mage/tabs.js b/lib/web/mage/tabs.js index b441477ab8d8a..65c452d33bf12 100644 --- a/lib/web/mage/tabs.js +++ b/lib/web/mage/tabs.js @@ -72,7 +72,7 @@ define([ if (anchor && isValid) { $.each(self.contents, function (i) { - if ($(this).attr('id') === anchorId) { + if ($(this).attr('id') === anchorId || $(this).find('#' + anchorId).length) { self.collapsibles.not(self.collapsibles.eq(i)).collapsible('forceDeactivate'); return false; diff --git a/pub/media/.htaccess b/pub/media/.htaccess index 28e65b490fbb8..d8793a891430a 100644 --- a/pub/media/.htaccess +++ b/pub/media/.htaccess @@ -23,6 +23,9 @@ SetHandler default-handler Options +FollowSymLinks RewriteEngine on + ## you can put here your pub/media folder path relative to web root + #RewriteBase /magento/pub/media/ + ############################################ ## never rewrite for existing files RewriteCond %{REQUEST_FILENAME} !-f diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php index 3b3fbf33a02e2..b5fbfd795363f 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php @@ -124,7 +124,7 @@ class Session implements ConfigOptionsListInterface ]; /** - * {@inheritdoc} + * @inheritdoc * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getOptions() @@ -250,7 +250,7 @@ public function getOptions() } /** - * {@inheritdoc} + * @inheritdoc */ public function createConfig(array $options, DeploymentConfig $deploymentConfig) { @@ -281,7 +281,7 @@ public function createConfig(array $options, DeploymentConfig $deploymentConfig) } /** - * {@inheritdoc} + * @inheritdoc */ public function validate(array $options, DeploymentConfig $deploymentConfig) { @@ -301,7 +301,7 @@ public function validate(array $options, DeploymentConfig $deploymentConfig) if (isset($options[self::INPUT_KEY_SESSION_REDIS_LOG_LEVEL])) { $level = $options[self::INPUT_KEY_SESSION_REDIS_LOG_LEVEL]; - if (($level < 0) or ($level > 7)) { + if (($level < 0) || ($level > 7)) { $errors[] = "Invalid Redis log level '{$level}'. Valid range is 0-7, inclusive."; } }