diff --git a/README.md b/README.md index a2cf536bb6520..6b2ac458eb403 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ To learn more about issue gate labels click [here](https://github.com/magento/ma

Reporting security issues

-To report security vulnerabilities in Magento software or web sites, please e-mail security@magento.com. Please do not report security issues using GitHub. Be sure to encrypt your e-mail with our encryption key if it includes sensitive information. Learn more about reporting security issues here. +To report security vulnerabilities in Magento software or web sites, please create a Bugcrowd researcher account there to submit and follow-up your issue. Learn more about reporting security issues here. Stay up-to-date on the latest security news and patches for Magento by signing up for Security Alert Notifications. diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index a92df095036f3..fda6ae9530135 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -104,7 +104,6 @@ class AdvancedPricing extends \Magento\CatalogImportExport\Model\Export\Product * @param \Magento\CatalogImportExport\Model\Export\RowCustomizerInterface $rowCustomizer * @param ImportProduct\StoreResolver $storeResolver * @param \Magento\Customer\Api\GroupRepositoryInterface $groupRepository - * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -193,6 +192,7 @@ protected function initTypeModels() * Export process * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ public function export() { @@ -586,8 +586,8 @@ protected function getTierPrices(array $listSku, $table) * Get Website code. * * @param int $websiteId - * * @return string + * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getWebsiteCode(int $websiteId): string { @@ -617,8 +617,9 @@ protected function _getWebsiteCode(int $websiteId): string * * @param int $groupId * @param int $allGroups - * * @return string + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ protected function _getCustomerGroupById( int $groupId, diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 4663aea7a7dfc..2e17e734b1e60 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -8,7 +8,6 @@ use Magento\CatalogImportExport\Model\Import\Product as ImportProduct; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; -use Magento\Framework\App\ResourceConnection; /** * Class AdvancedPricing @@ -618,6 +617,7 @@ protected function processCountNewPrices(array $tierPrices) * Get product entity link field * * @return string + * @throws \Exception */ private function getProductEntityLinkField() { diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php index 25a9fc244fe51..d939a3f7c392e 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator.php @@ -28,6 +28,7 @@ public function __construct($validators = []) * * @param array $value * @return bool + * @throws \Zend_Validate_Exception */ public function isValid($value) { diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php index 48b4c58918740..57ceb7f5af275 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Export/AdvancedPricingTest.php @@ -151,10 +151,13 @@ protected function setUp() ] ); $this->exportConfig = $this->createMock(\Magento\ImportExport\Model\Export\Config::class); - $this->productFactory = $this->createPartialMock(\Magento\Catalog\Model\ResourceModel\ProductFactory::class, [ + $this->productFactory = $this->createPartialMock( + \Magento\Catalog\Model\ResourceModel\ProductFactory::class, + [ 'create', 'getTypeId', - ]); + ] + ); $this->attrSetColFactory = $this->createPartialMock( \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory::class, [ @@ -185,11 +188,14 @@ protected function setUp() \Magento\CatalogImportExport\Model\Import\Product\StoreResolver::class ); $this->groupRepository = $this->createMock(\Magento\Customer\Api\GroupRepositoryInterface::class); - $this->writer = $this->createPartialMock(\Magento\ImportExport\Model\Export\Adapter\AbstractAdapter::class, [ - 'setHeaderCols', - 'writeRow', - 'getContents', - ]); + $this->writer = $this->createPartialMock( + \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter::class, + [ + 'setHeaderCols', + 'writeRow', + 'getContents', + ] + ); $constructorMethods = [ 'initTypeModels', 'initAttributes', @@ -213,7 +219,7 @@ protected function setUp() '_getCustomerGroupById', 'correctExportData' ]); - $this->advancedPricing = $this->getMockbuilder( + $this->advancedPricing = $this->getMockBuilder( \Magento\AdvancedPricingImportExport\Model\Export\AdvancedPricing::class ) ->setMethods($mockMethods) @@ -347,6 +353,7 @@ protected function tearDown() * @param $object * @param $property * @return mixed + * @throws \ReflectionException */ protected function getPropertyValue($object, $property) { @@ -362,6 +369,8 @@ protected function getPropertyValue($object, $property) * @param $object * @param $property * @param $value + * @return mixed + * @throws \ReflectionException */ protected function setPropertyValue(&$object, $property, $value) { diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php index 7a81ccae6f0d0..2c930237da831 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricing/Validator/TierPriceTest.php @@ -346,6 +346,7 @@ public function isValidAddMessagesCallDataProvider() * @param object $object * @param string $property * @return mixed + * @throws \ReflectionException */ protected function getPropertyValue($object, $property) { @@ -363,6 +364,7 @@ protected function getPropertyValue($object, $property) * @param string $property * @param mixed $value * @return object + * @throws \ReflectionException */ protected function setPropertyValue(&$object, $property, $value) { diff --git a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php index 08743b9fa7f2c..340e81746f029 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php @@ -209,6 +209,10 @@ public function testGetEntityTypeCode() * Test method validateRow against its result. * * @dataProvider validateRowResultDataProvider + * @param array $rowData + * @param string|null $behavior + * @param bool $expectedResult + * @throws \ReflectionException */ public function testValidateRowResult($rowData, $behavior, $expectedResult) { @@ -234,6 +238,10 @@ public function testValidateRowResult($rowData, $behavior, $expectedResult) * Test method validateRow whether AddRowError is called. * * @dataProvider validateRowAddRowErrorCallDataProvider + * @param array $rowData + * @param string|null $behavior + * @param string $error + * @throws \ReflectionException */ public function testValidateRowAddRowErrorCall($rowData, $behavior, $error) { @@ -324,6 +332,13 @@ public function testSaveAdvancedPricing() * Take into consideration different data and check relative internal calls. * * @dataProvider saveAndReplaceAdvancedPricesAppendBehaviourDataProvider + * @param array $data + * @param string $tierCustomerGroupId + * @param string $groupCustomerGroupId + * @param string $tierWebsiteId + * @param string $groupWebsiteId + * @param array $expectedTierPrices + * @throws \ReflectionException */ public function testSaveAndReplaceAdvancedPricesAppendBehaviourDataAndCalls( $data, @@ -956,6 +971,7 @@ public function processCountExistingPricesDataProvider() * @param $object * @param $property * @return mixed + * @throws \ReflectionException */ protected function getPropertyValue($object, $property) { @@ -972,6 +988,8 @@ protected function getPropertyValue($object, $property) * @param $object * @param $property * @param $value + * @return mixed + * @throws \ReflectionException */ protected function setPropertyValue(&$object, $property, $value) { @@ -989,8 +1007,8 @@ protected function setPropertyValue(&$object, $property, $value) * @param object $object * @param string $method * @param array $args - * - * @return mixed the method result. + * @return mixed + * @throws \ReflectionException */ private function invokeMethod($object, $method, $args = []) { @@ -1007,6 +1025,7 @@ private function invokeMethod($object, $method, $args = []) * @param array $methods * * @return \PHPUnit_Framework_MockObject_MockObject + * @throws \ReflectionException */ private function getAdvancedPricingMock($methods = []) { diff --git a/app/code/Magento/AdvancedSearch/Block/SearchData.php b/app/code/Magento/AdvancedSearch/Block/SearchData.php index 993731b465257..105a1c1c4fc46 100644 --- a/app/code/Magento/AdvancedSearch/Block/SearchData.php +++ b/app/code/Magento/AdvancedSearch/Block/SearchData.php @@ -30,7 +30,7 @@ abstract class SearchData extends Template implements SearchDataInterface /** * @var string */ - protected $_template = 'search_data.phtml'; + protected $_template = 'Magento_AdvancedSearch::search_data.phtml'; /** * @param Template\Context $context diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml index e4630c1d5b5a1..fc1ff7d18b51e 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml @@ -23,6 +23,7 @@ + diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php index cbf06264096ac..407e323aeaae6 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/AdditionalCommentTest.php @@ -65,11 +65,11 @@ public function testRender() ->method('getLabel') ->willReturn('Comment label'); $html = $this->additionalComment->render($this->abstractElementMock); - $this->assertRegexp( + $this->assertRegExp( "/New comment/", $html ); - $this->assertRegexp( + $this->assertRegExp( "/Comment label/", $html ); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php index 462b3c909a7fd..d567d65882350 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php @@ -87,7 +87,7 @@ public function testRender() $this->localeResolver->expects($this->once()) ->method('getLocale') ->willReturn('en_US'); - $this->assertRegexp( + $this->assertRegExp( "/Eastern Standard Time \(America\/New_York\)/", $this->collectionTimeLabel->render($this->abstractElementMock) ); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php index d643bc05cc615..78ff581f3de9d 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php @@ -74,7 +74,7 @@ public function testRender() $this->abstractElementMock->expects($this->any()) ->method('getComment') ->willReturn('Subscription status: Enabled'); - $this->assertRegexp( + $this->assertRegExp( "/Subscription status: Enabled/", $this->subscriptionStatusLabel->render($this->abstractElementMock) ); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php index abce48c36c86a..6a0cecc781062 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php @@ -65,7 +65,7 @@ public function testRender() ->method('getHint') ->willReturn('New hint'); $html = $this->vertical->render($this->abstractElementMock); - $this->assertRegexp( + $this->assertRegExp( "/New comment/", $html ); diff --git a/app/code/Magento/Authorizenet/Model/Directpost.php b/app/code/Magento/Authorizenet/Model/Directpost.php index 5476fd05a0fed..d5c11ab54cd94 100644 --- a/app/code/Magento/Authorizenet/Model/Directpost.php +++ b/app/code/Magento/Authorizenet/Model/Directpost.php @@ -814,10 +814,14 @@ protected function declineOrder(\Magento\Sales\Model\Order $order, $message = '' { try { $response = $this->getResponse(); - if ($voidPayment && $response->getXTransId() && strtoupper($response->getXType()) - == self::REQUEST_TYPE_AUTH_ONLY + if ($voidPayment + && $response->getXTransId() + && strtoupper($response->getXType()) == self::REQUEST_TYPE_AUTH_ONLY ) { - $order->getPayment()->setTransactionId(null)->setParentTransactionId($response->getXTransId())->void(); + $order->getPayment() + ->setTransactionId(null) + ->setParentTransactionId($response->getXTransId()) + ->void($response); } $order->registerCancellation($message)->save(); } catch (\Exception $e) { diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php index 723deab1e9f7e..eff49c3b75ab2 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php @@ -124,14 +124,18 @@ protected function _toHtml() if (!$this->_depends) { return ''; } - return ''; + + $params = $this->_getDependsJson(); + + if ($this->_configOptions) { + $params .= ', ' . $this->_jsonEncoder->encode($this->_configOptions); + } + + return ""; } /** diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php index 88d1560026cbd..1d8d658267020 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php @@ -26,7 +26,6 @@ public function getValue($index = null) { if ($index) { if ($data = $this->getData('value', 'orig_' . $index)) { - // date('Y-m-d', strtotime($data)); return $data; } return null; diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php index 3fa60faad45c8..185b1116b8f67 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/AbstractMassaction.php @@ -277,13 +277,13 @@ public function getGridIdsJson() } /** @var \Magento\Framework\Data\Collection $allIdsCollection */ $allIdsCollection = clone $this->getParentBlock()->getCollection(); - + if ($this->getMassactionIdField()) { $massActionIdField = $this->getMassactionIdField(); } else { $massActionIdField = $this->getParentBlock()->getMassactionIdField(); } - + $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); if (!empty($gridIds)) { return join(",", $gridIds); diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php index d59d3858179e0..8e0fce2b16cc9 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction/Extended.php @@ -274,13 +274,13 @@ public function getGridIdsJson() /** @var \Magento\Framework\Data\Collection $allIdsCollection */ $allIdsCollection = clone $this->getParentBlock()->getCollection(); - + if ($this->getMassactionIdField()) { $massActionIdField = $this->getMassactionIdField(); } else { $massActionIdField = $this->getParentBlock()->getMassactionIdField(); } - + $gridIds = $allIdsCollection->setPageSize(0)->getColumnValues($massActionIdField); if (!empty($gridIds)) { diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml index ac35f3f8f6b1f..ff5e02397cbff 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMessagesSection.xml @@ -10,5 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd">
+ +
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml index 724bf472de0f5..ea84ce7ea0c4f 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminSecondaryGridSection.xml @@ -11,7 +11,9 @@
+
+ diff --git a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml index 46902363f1fd7..79c987383299f 100644 --- a/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml +++ b/app/code/Magento/Backend/view/adminhtml/ui_component/design_config_form.xml @@ -92,7 +92,7 @@ - + false diff --git a/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php index e89e604867baa..32abeac4c8ffb 100644 --- a/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/CardDetailsHandler.php @@ -85,7 +85,7 @@ public function handle(array $handlingSubject, array $response) private function getCreditCardType($type) { $replaced = str_replace(' ', '-', strtolower($type)); - $mapper = $this->config->getCctypesMapper(); + $mapper = $this->config->getCcTypesMapper(); return $mapper[$replaced]; } diff --git a/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php b/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php index 7c307185b4c22..8880f9c1b1a3e 100644 --- a/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php +++ b/app/code/Magento/Braintree/Gateway/Response/VaultDetailsHandler.php @@ -157,7 +157,7 @@ private function convertDetailsToJSON($details) private function getCreditCardType($type) { $replaced = str_replace(' ', '-', strtolower($type)); - $mapper = $this->config->getCctypesMapper(); + $mapper = $this->config->getCcTypesMapper(); return $mapper[$replaced]; } diff --git a/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php b/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php index fe30e790de07c..928769498a035 100644 --- a/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php +++ b/app/code/Magento/Braintree/Model/Ui/ConfigProvider.php @@ -70,7 +70,7 @@ public function getConfig() self::CODE => [ 'isActive' => $this->config->isActive($storeId), 'clientToken' => $this->getClientToken(), - 'ccTypesMapper' => $this->config->getCctypesMapper(), + 'ccTypesMapper' => $this->config->getCcTypesMapper(), 'sdkUrl' => $this->config->getSdkUrl(), 'countrySpecificCardTypes' => $this->config->getCountrySpecificCardTypeConfig($storeId), 'availableCardTypes' => $this->config->getAvailableCardTypes($storeId), diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Config/ConfigTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Config/ConfigTest.php index 7b9d59a5bc482..36ea3aea465dd 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Config/ConfigTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Config/ConfigTest.php @@ -142,7 +142,7 @@ public function testGetCcTypesMapper($value, $expected) static::assertEquals( $expected, - $this->model->getCctypesMapper() + $this->model->getCcTypesMapper() ); } diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json index 5854a717513ba..8fb98dce72c92 100644 --- a/app/code/Magento/Braintree/composer.json +++ b/app/code/Magento/Braintree/composer.json @@ -6,7 +6,7 @@ }, "require": { "php": "~7.1.3||~7.2.0", - "braintree/braintree_php": "3.28.0", + "braintree/braintree_php": "3.34.0", "magento/framework": "*", "magento/magento-composer-installer": "*", "magento/module-catalog": "*", diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index f5e05cbc3e212..17ecba545efad 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -534,12 +534,12 @@ public function updateQtyOption($options, \Magento\Framework\DataObject $option, foreach ($selections as $selection) { if ($selection->getProductId() == $optionProduct->getId()) { - foreach ($options as &$option) { - if ($option->getCode() == 'selection_qty_' . $selection->getSelectionId()) { + foreach ($options as $quoteItemOption) { + if ($quoteItemOption->getCode() == 'selection_qty_' . $selection->getSelectionId()) { if ($optionUpdateFlag) { - $option->setValue(intval($option->getValue())); + $quoteItemOption->setValue(intval($quoteItemOption->getValue())); } else { - $option->setValue($value); + $quoteItemOption->setValue($value); } } } diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml index e104803fd405d..af8fc1459d9e3 100644 --- a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/CreateBundleProductActionGroup.xml @@ -21,4 +21,38 @@
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml new file mode 100644 index 0000000000000..48697d43ec824 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml index 41006ee9b4d09..80ffe13ff3fe4 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml @@ -26,6 +26,7 @@ 20 4 bundle + 10 10 $10.00 Default diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index 5f8cde2997c08..0d0edbf857515 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -21,6 +21,7 @@ + @@ -60,5 +61,12 @@ + + + + + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml index 311201e624d4b..b462fcd11cfe7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml @@ -18,5 +18,8 @@ + + + diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductActionSection.xml new file mode 100644 index 0000000000000..abc9bc6dab540 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductActionSection.xml @@ -0,0 +1,15 @@ + + + +
+ + + +
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml index d967143258918..e1434e22489ce 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml @@ -75,11 +75,16 @@ - + + + + + + - + diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml new file mode 100644 index 0000000000000..655081df61073 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml @@ -0,0 +1,231 @@ + + + + + + + + + + <description value="Customer should not be able to add a Bundle Product to the cart without selecting options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-233"/> + <group value="Bundle"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + </before> + <after> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + + <!-- Start creating a bundle product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillNameAndSku"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!-- Add Option One, a "Drop-down" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts1"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option One"/> + <argument name="inputType" value="select"/> + </actionGroup> + + <!-- Add Option Two, a "Radio Buttons" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts2"> + <argument name="x" value="1"/> + <argument name="n" value="2"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option Two"/> + <argument name="inputType" value="radio"/> + </actionGroup> + + <!-- Add Option Three, a "Checkbox" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts3"> + <argument name="x" value="2"/> + <argument name="n" value="3"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option Three"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + + <!-- Add Option Four, a "Multi Select" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts4"> + <argument name="x" value="3"/> + <argument name="n" value="4"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option Four"/> + <argument name="inputType" value="multi"/> + </actionGroup> + + <!-- Save product and go to storefront --> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!-- See validation errors for all 4 options --> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddToCart1"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('1')}}" userInput="This is a required field." stepKey="error1"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('2')}}" userInput="Please select one of the options." stepKey="error2"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('3')}}" userInput="Please select one of the options." stepKey="error3"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('4')}}" userInput="This is a required field." stepKey="error4"/> + + <!-- Fill option 1, see validation errors for 3 other options --> + <selectOption selector="select.bundle-option-select" userInput="$$simpleProduct1.name$$ +$123.00" stepKey="selectOption1"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddToCart2"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('1')}}" userInput="This is a required field." stepKey="error5"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('2')}}" userInput="Please select one of the options." stepKey="error6"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('3')}}" userInput="Please select one of the options." stepKey="error7"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('4')}}" userInput="This is a required field." stepKey="error8"/> + + <!-- Fill option 2, see validation errors for 2 other options --> + <click selector="input[type='radio']:nth-of-type(1)" stepKey="selectOption2"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddToCart3"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('1')}}" userInput="This is a required field." stepKey="error9"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('2')}}" userInput="Please select one of the options." stepKey="error10"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('3')}}" userInput="Please select one of the options." stepKey="error11"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('4')}}" userInput="This is a required field." stepKey="error12"/> + + <!-- Fill option 3, see validation errors for 1 other options --> + <checkOption selector="input[type='checkbox']:nth-of-type(1)" stepKey="selectOption3"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddToCart4"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('1')}}" userInput="This is a required field." stepKey="error13"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('2')}}" userInput="Please select one of the options." stepKey="error14"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('3')}}" userInput="Please select one of the options." stepKey="error15"/> + <see selector="{{StorefrontBundledSection.nthOptionDiv('4')}}" userInput="This is a required field." stepKey="error16"/> + + <!-- Fill option 4, dont see any validation errors --> + <selectOption selector="select[multiple='multiple']" userInput="$$simpleProduct1.name$$ +$123.00" stepKey="selectOption4"/> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddToCart5"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('1')}}" userInput="This is a required field." stepKey="error17"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('2')}}" userInput="Please select one of the options." stepKey="error18"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('3')}}" userInput="Please select one of the options." stepKey="error19"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('4')}}" userInput="This is a required field." stepKey="error20"/> + </test> + + <test name="StorefrontBundleAddToCartSuccessTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product details page"/> + <title value="Customer should be able to add the bundle product to the cart"/> + <description value="Customer should be able to add the bundle product to the cart"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-232"/> + <group value="Bundle"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + </before> + <after> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + + <!-- Start creating a bundle product --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> + <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProduct"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="fillProductNameAndSkuInProductForm" stepKey="fillNameAndSku"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + + <!-- Add Option One, a "Drop-down" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts1"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option One"/> + <argument name="inputType" value="select"/> + </actionGroup> + + <!-- Add Option Two, a "Radio Buttons" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts2"> + <argument name="x" value="1"/> + <argument name="n" value="2"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option Two"/> + <argument name="inputType" value="radio"/> + </actionGroup> + + <!-- Add Option Three, a "Checkbox" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts3"> + <argument name="x" value="2"/> + <argument name="n" value="3"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option Three"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + + <!-- Add Option Four, a "Multi Select" type option --> + <actionGroup ref="addBundleOptionWithTwoProducts" stepKey="addBundleOptionWithTwoProducts4"> + <argument name="x" value="3"/> + <argument name="n" value="4"/> + <argument name="prodOneSku" value="$$simpleProduct1.sku$$"/> + <argument name="prodTwoSku" value="$$simpleProduct2.sku$$"/> + <argument name="optionTitle" value="Option Four"/> + <argument name="inputType" value="multi"/> + </actionGroup> + + <!-- Save product and go to storefront --> + <actionGroup ref="saveProductForm" stepKey="saveProduct"/> + <amOnPage url="{{BundleProduct.sku}}.html" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!-- Select all applicable options --> + <selectOption selector="select.bundle-option-select" userInput="$$simpleProduct1.name$$ +$123.00" stepKey="selectOption1"/> + <click selector="input[type='radio']:nth-of-type(1)" stepKey="selectOption2"/> + <checkOption selector="input[type='checkbox']:nth-of-type(1)" stepKey="selectOption3"/> + <selectOption selector="select[multiple='multiple']" userInput="$$simpleProduct1.name$$ +$123.00" stepKey="selectOption4"/> + + <!-- Customize and add the bundle product to our cart --> + <click selector="{{StorefrontBundledSection.addToCartConfigured}}" stepKey="clickAddToCart1"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('1')}}" userInput="This is a required field." stepKey="validForm1"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('2')}}" userInput="Please select one of the options." stepKey="validForm2"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('3')}}" userInput="Please select one of the options." stepKey="validForm3"/> + <dontSee selector="{{StorefrontBundledSection.nthOptionDiv('4')}}" userInput="This is a required field." stepKey="validForm4"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{BundleProduct.name}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> + + <!-- Verify cart contents --> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> + <waitForPageLoad stepKey="waitForCart"/> + <see selector="{{StorefrontBundledSection.nthItemOptionsTitle('1')}}" userInput="Option One" stepKey="seeOption1"/> + <see selector="{{StorefrontBundledSection.nthItemOptionsTitle('2')}}" userInput="Option Two" stepKey="seeOption2"/> + <see selector="{{StorefrontBundledSection.nthItemOptionsTitle('3')}}" userInput="Option Three" stepKey="seeOption3"/> + <see selector="{{StorefrontBundledSection.nthItemOptionsTitle('4')}}" userInput="Option Four" stepKey="seeOption4"/> + <see selector="{{StorefrontBundledSection.nthItemOptionsValue('1')}}" userInput="50 x $$simpleProduct1.name$$ $123.00" stepKey="seeOptionValue1"/> + <see selector="{{StorefrontBundledSection.nthItemOptionsValue('2')}}" userInput="50 x $$simpleProduct1.name$$ $123.00" stepKey="seeOptionValue2"/> + <see selector="{{StorefrontBundledSection.nthItemOptionsValue('3')}}" userInput="50 x $$simpleProduct1.name$$ $123.00" stepKey="seeOptionValue3"/> + <see selector="{{StorefrontBundledSection.nthItemOptionsValue('4')}}" userInput="50 x $$simpleProduct1.name$$ $123.00" stepKey="seeOptionValue4"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml new file mode 100644 index 0000000000000..577079965cabb --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGrid.xml @@ -0,0 +1,101 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontBundleProductShownInCategoryListAndGrid"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle products list on Storefront"/> + <title value="Customer should be able to see bundle products in the category products list and grid views"/> + <description value="Customer should be able to see bundle products in the category products list and grid views"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-226"/> + <group value="Bundle"/> + </annotations> + <before> + <!--Admin login--> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct3"/> + <createData entity="SimpleProduct2" stepKey="simpleProduct4"/> + </before> + <after> + <!--Logging out--> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + </after> + <!--Make category--> + <amOnPage url="{{AdminCategoryPage.url}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="CreateCategory" stepKey="createASubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> + + <!--Go to bundle product creation page--> + <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage" /> + <waitForPageLoad stepKey="waitForBundleProductCreatePageToLoad"/> + + <!--Categories--> + <click selector="{{AdminProductFormBundleSection.categoriesDropDown}}" stepKey="dropDownCategories"/> + <fillField selector="{{AdminProductFormBundleSection.searchForCategory}}" userInput="{{SimpleSubCategory.name}}" stepKey="searchForCategory"/> + <click selector="{{AdminProductFormBundleSection.selectCategory}}" stepKey="selectCategory"/> + <click selector="{{AdminProductFormBundleSection.categoriesLabel}}" stepKey="clickOnCategoriesLabelToCloseOptions"/> + + <!--Create bundle product--> + <conditionalClick selector="{{AdminProductFormBundleSection.bundleItemsToggle}}" dependentSelector="{{AdminProductFormBundleSection.bundleItemsToggle}}" visible="false" stepKey="conditionallyOpenSectionBundleItems"/> + <click selector="{{AdminProductFormBundleSection.addOption}}" stepKey="clickAddOption3"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" stepKey="waitForBundleOptions"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="{{BundleProduct.optionTitle1}}" stepKey="fillOptionTitle"/> + <selectOption selector="{{AdminProductFormBundleSection.bundleOptionXInputType('0')}}" userInput="{{BundleProduct.optionInputType1}}" stepKey="selectInputType"/> + <waitForElementVisible selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="waitForAddProductsToBundle"/> + <click selector="{{AdminProductFormBundleSection.addProductsToOption}}" stepKey="clickAddProductsToOption"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterBundleProductOptions"> + <argument name="product" value="$$simpleProduct1$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow"/> + <actionGroup ref="filterProductGridBySku" stepKey="filterBundleProductOptions2"> + <argument name="product" value="$$simpleProduct2$$"/> + </actionGroup> + <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow2"/> + <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty1"/> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty2"/> + <actionGroup ref="AncillaryPrepBundleProduct" stepKey="createBundledProductForTwoSimpleProducts"/> + + <!--Save the product--> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> + + <!--Go to category page--> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForHomePageToload"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="cartClickCategory"/> + + <!--Check in grid view--> + <seeInTitle userInput="{{SimpleSubCategory.name}}" stepKey="assertCategoryNameInTitle"/> + <see userInput="{{SimpleSubCategory.name}}" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="assertCategoryName"/> + <see userInput="1" selector="{{StorefrontCategoryMainSection.productCount}} span" stepKey="assertProductCount"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProduct('1')}}" stepKey="assertBundleProductPresence"/> + <see userInput="{{BundleProduct.name}}" selector="{{StorefrontCategoryProductSection.ProductTitleByNumber('1')}}" stepKey="checkTitle"/> + <see userInput="$1,230.00" selector="{{StorefrontCategoryProductSection.ProductPriceByNumber('1')}}" stepKey="checkPrice"/> + + <!--Check in list view--> + <click selector="{{StorefrontCategoryProductSection.categoryListView}}" stepKey="switchToListView"/> + <seeInTitle userInput="{{SimpleSubCategory.name}}" stepKey="assertCategoryNameInTitleAgain"/> + <see userInput="{{SimpleSubCategory.name}}" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="assertCategoryNamAgain"/> + <see userInput="1" selector="{{StorefrontCategoryMainSection.productCount}} span" stepKey="assertProductCountAgain"/> + <seeElement selector="{{StorefrontCategoryProductSection.listedProduct('1')}}" stepKey="assertBundleProductPresenceAgain"/> + <see userInput="{{BundleProduct.name}}" selector="{{StorefrontCategoryProductSection.ProductTitleByNumber('1')}}" stepKey="checkTitleAgain"/> + <see userInput="$1,230.00" selector="{{StorefrontCategoryProductSection.ProductPriceByNumber('1')}}" stepKey="checkPriceAgain"/> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckGuestCheckoutObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckGuestCheckoutObserverTest.php new file mode 100644 index 0000000000000..d3f29fae8a592 --- /dev/null +++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckGuestCheckoutObserverTest.php @@ -0,0 +1,211 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Captcha\Test\Unit\Observer; + +use Magento\Captcha\Model\DefaultModel as CaptchaModel; +use Magento\Captcha\Observer\CheckGuestCheckoutObserver; +use Magento\Captcha\Helper\Data as CaptchaDataHelper; +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\ActionFlag; +use Magento\Captcha\Observer\CaptchaStringResolver; +use Magento\Checkout\Model\Type\Onepage; +use Magento\Framework\App\Request\Http; +use Magento\Framework\App\Response\Http as HttpResponse; +use Magento\Framework\Event\Observer; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\Quote; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CheckGuestCheckoutObserverTest extends \PHPUnit\Framework\TestCase +{ + const FORM_ID = 'guest_checkout'; + + /** + * @var CheckGuestCheckoutObserver + */ + private $checkGuestCheckoutObserver; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Observer + */ + private $observer; + + /** + * @var HttpResponse|\PHPUnit_Framework_MockObject_MockObject + */ + private $responseMock; + + /** + * @var HttpResponse|\PHPUnit_Framework_MockObject_MockObject + */ + private $requestMock; + + /** + * @var ActionFlag|\PHPUnit_Framework_MockObject_MockObject + */ + private $actionFlagMock; + + /** + * @var CaptchaStringResolver|\PHPUnit_Framework_MockObject_MockObject + */ + private $captchaStringResolverMock; + + /** + * @var JsonHelper|\PHPUnit_Framework_MockObject_MockObject + */ + private $jsonHelperMock; + + /** + * @var CaptchaModel|\PHPUnit_Framework_MockObject_MockObject + */ + private $captchaModelMock; + + /** + * @var Quote|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteModelMock; + + /** + * @var Action|\PHPUnit_Framework_MockObject_MockObject + */ + private $controllerMock; + + protected function setUp() + { + $onepageModelTypeMock = $this->createMock(Onepage::class); + $captchaHelperMock = $this->createMock(CaptchaDataHelper::class); + $this->objectManager = new ObjectManager($this); + $this->actionFlagMock = $this->createMock(ActionFlag::class); + $this->captchaStringResolverMock = $this->createMock(CaptchaStringResolver::class); + $this->captchaModelMock = $this->createMock(CaptchaModel::class); + $this->quoteModelMock = $this->createMock(Quote::class); + $this->controllerMock = $this->createMock(Action::class); + $this->requestMock = $this->createMock(Http::class); + $this->responseMock = $this->createMock(HttpResponse::class); + $this->observer = new Observer(['controller_action' => $this->controllerMock]); + $this->jsonHelperMock = $this->createMock(JsonHelper::class); + + $this->checkGuestCheckoutObserver = $this->objectManager->getObject( + CheckGuestCheckoutObserver::class, + [ + 'helper' => $captchaHelperMock, + 'actionFlag' => $this->actionFlagMock, + 'captchaStringResolver' => $this->captchaStringResolverMock, + 'typeOnepage' => $onepageModelTypeMock, + 'jsonHelper' => $this->jsonHelperMock + ] + ); + + $captchaHelperMock->expects($this->once()) + ->method('getCaptcha') + ->with(self::FORM_ID) + ->willReturn($this->captchaModelMock); + $onepageModelTypeMock->expects($this->once()) + ->method('getQuote') + ->willReturn($this->quoteModelMock); + } + + public function testCheckGuestCheckoutForRegister() + { + $this->quoteModelMock->expects($this->once()) + ->method('getCheckoutMethod') + ->willReturn(Onepage::METHOD_REGISTER); + $this->captchaModelMock->expects($this->never()) + ->method('isRequired'); + + $this->checkGuestCheckoutObserver->execute($this->observer); + } + + public function testCheckGuestCheckoutWithNoCaptchaRequired() + { + $this->quoteModelMock->expects($this->once()) + ->method('getCheckoutMethod') + ->willReturn(Onepage::METHOD_GUEST); + $this->captchaModelMock->expects($this->once()) + ->method('isRequired') + ->willReturn(false); + $this->captchaModelMock->expects($this->never()) + ->method('isCorrect'); + + $this->checkGuestCheckoutObserver->execute($this->observer); + } + + public function testCheckGuestCheckoutWithIncorrectCaptcha() + { + $captchaValue = 'some_word'; + $encodedJsonValue = '{}'; + + $this->quoteModelMock->expects($this->once()) + ->method('getCheckoutMethod') + ->willReturn(Onepage::METHOD_GUEST); + $this->captchaModelMock->expects($this->once()) + ->method('isRequired') + ->willReturn(true); + $this->controllerMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->controllerMock->expects($this->once()) + ->method('getResponse') + ->willReturn($this->responseMock); + $this->controllerMock->expects($this->once()) + ->method('getResponse') + ->willReturn($this->responseMock); + $this->captchaStringResolverMock->expects($this->once()) + ->method('resolve') + ->with($this->requestMock, self::FORM_ID) + ->willReturn($captchaValue); + $this->captchaModelMock->expects($this->once()) + ->method('isCorrect') + ->with($captchaValue) + ->willReturn(false); + $this->actionFlagMock->expects($this->once()) + ->method('set') + ->with('', Action::FLAG_NO_DISPATCH, true); + $this->jsonHelperMock->expects($this->once()) + ->method('jsonEncode') + ->willReturn($encodedJsonValue); + $this->responseMock->expects($this->once()) + ->method('representJson') + ->with($encodedJsonValue); + + $this->checkGuestCheckoutObserver->execute($this->observer); + } + + public function testCheckGuestCheckoutWithCorrectCaptcha() + { + $this->quoteModelMock->expects($this->once()) + ->method('getCheckoutMethod') + ->willReturn(Onepage::METHOD_GUEST); + $this->captchaModelMock->expects($this->once()) + ->method('isRequired') + ->willReturn(true); + $this->controllerMock->expects($this->once()) + ->method('getRequest') + ->willReturn($this->requestMock); + $this->captchaStringResolverMock->expects($this->once()) + ->method('resolve') + ->with($this->requestMock, self::FORM_ID) + ->willReturn('some_word'); + $this->captchaModelMock->expects($this->once()) + ->method('isCorrect') + ->with('some_word') + ->willReturn(true); + $this->actionFlagMock->expects($this->never()) + ->method('set'); + + $this->checkGuestCheckoutObserver->execute($this->observer); + } +} diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php index f5e3f94418687..cb0a739b56e4e 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Set/Main/Tree/Attribute.php @@ -14,5 +14,5 @@ class Attribute extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'catalog/product/attribute/set/main/tree/attribute.phtml'; + protected $_template = 'Magento_Catalog::catalog/product/attribute/set/main/tree/attribute.phtml'; } diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php index d582005f653ef..181211a0fc4a2 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -105,9 +105,11 @@ public function getOption() } /** + * Retrieve formatted price + * * @return string */ - public function getFormatedPrice() + public function getFormattedPrice() { if ($option = $this->getOption()) { return $this->_formatPrice( @@ -120,6 +122,17 @@ public function getFormatedPrice() return ''; } + /** + * @return string + * + * @deprecated + * @see getFormattedPrice() + */ + public function getFormatedPrice() + { + return $this->getFormattedPrice(); + } + /** * Return formated price * diff --git a/app/code/Magento/Catalog/Block/Product/View/Price.php b/app/code/Magento/Catalog/Block/Product/View/Price.php index c38625247b533..37598dfb1a8da 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Price.php +++ b/app/code/Magento/Catalog/Block/Product/View/Price.php @@ -9,6 +9,8 @@ */ namespace Magento\Catalog\Block\Product\View; +use Magento\Catalog\Model\Product; + class Price extends \Magento\Framework\View\Element\Template { /** @@ -37,7 +39,8 @@ public function __construct( */ public function getPrice() { + /** @var Product $product */ $product = $this->_coreRegistry->registry('product'); - return $product->getFormatedPrice(); + return $product->getFormattedPrice(); } } diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index f514e5c68769e..90af9bee270bd 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -1124,11 +1124,24 @@ public function getTierPrice($qty = null) /** * Get formatted by currency product price * - * @return array || double + * @return array|double + */ + public function getFormattedPrice() + { + return $this->getPriceModel()->getFormattedPrice($this); + } + + /** + * Get formatted by currency product price + * + * @return array|double + * + * @deprecated + * @see getFormattedPrice() */ public function getFormatedPrice() { - return $this->getPriceModel()->getFormatedPrice($this); + return $this->getFormattedPrice(); } /** diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index 39595cdaa60ad..acfc454883e1d 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -8,6 +8,7 @@ use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; @@ -102,6 +103,11 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter */ private $metadataPool; + /** + * @var ProductCustomOptionValuesInterfaceFactory + */ + private $customOptionValuesFactory; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -114,6 +120,7 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -127,12 +134,16 @@ public function __construct( Option\Validator\Pool $validatorPool, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + array $data = [], + ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null ) { $this->productOptionValue = $productOptionValue; $this->optionTypeFactory = $optionFactory; $this->validatorPool = $validatorPool; $this->string = $string; + $this->customOptionValuesFactory = $customOptionValuesFactory ?: + \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); + parent::__construct( $context, $registry, @@ -390,20 +401,21 @@ public function beforeSave() */ public function afterSave() { - $this->getValueInstance()->unsetValues(); $values = $this->getValues() ?: $this->getData('values'); if (is_array($values)) { foreach ($values as $value) { - if ($value instanceof \Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface) { + if ($value instanceof ProductCustomOptionValuesInterface) { $data = $value->getData(); } else { $data = $value; } - $this->getValueInstance()->addValue($data); - } - $this->getValueInstance()->setOption($this)->saveValues(); - } elseif ($this->getGroupByType($this->getType()) == self::OPTION_GROUP_SELECT) { + $this->customOptionValuesFactory->create() + ->addValue($data) + ->setOption($this) + ->saveValues(); + } + } elseif ($this->getGroupByType($this->getType()) === self::OPTION_GROUP_SELECT) { throw new LocalizedException(__('Select type options required values rows.')); } @@ -804,7 +816,7 @@ public function setImageSizeY($imageSizeY) } /** - * @param \Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface[] $values + * @param ProductCustomOptionValuesInterface[] $values * @return $this */ public function setValues(array $values = null) diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php index fd0eae188fea9..9ffe75e513bce 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Text.php @@ -84,7 +84,7 @@ public function validateUserValue($values) */ public function prepareForCart() { - if ($this->getIsValid() && strlen($this->getUserValue()) > 0) { + if ($this->getIsValid() && ($this->getUserValue() !== '')) { return $this->getUserValue(); } else { return null; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index fb7759b210bd9..ebbc060c99edf 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -76,6 +76,7 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data + * @param CustomOptionPriceCalculator|null $customOptionPriceCalculator */ public function __construct( \Magento\Framework\Model\Context $context, @@ -89,6 +90,7 @@ public function __construct( $this->_valueCollectionFactory = $valueCollectionFactory; $this->customOptionPriceCalculator = $customOptionPriceCalculator ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); + parent::__construct( $context, $registry, @@ -201,19 +203,15 @@ public function getProduct() */ public function saveValues() { + $option = $this->getOption(); + foreach ($this->getValues() as $value) { $this->isDeleted(false); - $this->setData( - $value - )->setData( - 'option_id', - $this->getOption()->getId() - )->setData( - 'store_id', - $this->getOption()->getStoreId() - ); - - if ($this->getData('is_delete') == '1') { + $this->setData($value) + ->setData('option_id', $option->getId()) + ->setData('store_id', $option->getStoreId()); + + if ((bool) $this->getData('is_delete') === true) { if ($this->getId()) { $this->deleteValues($this->getId()); $this->delete(); @@ -222,7 +220,7 @@ public function saveValues() $this->save(); } } - //eof foreach() + return $this; } diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index 7eaedf77eb859..f6caa299d66d7 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -474,14 +474,15 @@ public function getTierPriceCount($product) * * @param float $qty * @param Product $product + * * @return array|float */ - public function getFormatedTierPrice($qty, $product) + public function getFormattedTierPrice($qty, $product) { $price = $product->getTierPrice($qty); if (is_array($price)) { foreach (array_keys($price) as $index) { - $price[$index]['formated_price'] = $this->priceCurrency->convertAndFormat( + $price[$index]['formatted_price'] = $this->priceCurrency->convertAndFormat( $price[$index]['website_price'] ); } @@ -492,15 +493,45 @@ public function getFormatedTierPrice($qty, $product) return $price; } + /** + * Get formatted by currency tier price + * + * @param float $qty + * @param Product $product + * + * @return array|float + * + * @deprecated + * @see getFormattedTierPrice() + */ + public function getFormatedTierPrice($qty, $product) + { + return $this->getFormattedTierPrice($qty, $product); + } + + /** + * Get formatted by currency product price + * + * @param Product $product + * @return array|float + */ + public function getFormattedPrice($product) + { + return $this->priceCurrency->format($product->getFinalPrice()); + } + /** * Get formatted by currency product price * * @param Product $product * @return array || float + * + * @deprecated + * @see getFormattedPrice() */ public function getFormatedPrice($product) { - return $this->priceCurrency->format($product->getFinalPrice()); + return $this->getFormattedPrice($product); } /** diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php index bcb6638b9cd25..83d59718400bd 100644 --- a/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredRegularPrice.php @@ -63,7 +63,7 @@ public function setItem(ItemInterface $item) : ConfiguredRegularPrice return $this; } - + /** * Price value of product with configured options. * @@ -73,7 +73,7 @@ public function getValue() { $basePrice = parent::getValue(); - return $this->item + return $this->item && $basePrice !== false ? $basePrice + $this->configuredOptions->getItemOptionsValue($basePrice, $this->item) : $basePrice; } diff --git a/app/code/Magento/Catalog/Setup/Patch/Data/UpgradeWebsiteAttributes.php b/app/code/Magento/Catalog/Setup/Patch/Data/UpgradeWebsiteAttributes.php index f9d6abbc37493..a190bde2c6775 100644 --- a/app/code/Magento/Catalog/Setup/Patch/Data/UpgradeWebsiteAttributes.php +++ b/app/code/Magento/Catalog/Setup/Patch/Data/UpgradeWebsiteAttributes.php @@ -156,6 +156,21 @@ private function processAttributeValues(array $attributeValueItems, $tableName) */ private function fetchAttributeValues($tableName) { + //filter store groups which have more than 1 store + $multipleStoresInWebsite = array_values( + array_reduce( + array_filter($this->getGroupedStoreViews(), function ($storeViews) { + return is_array($storeViews) && count($storeViews) > 1; + }), + 'array_merge', + [] + ) + ); + + if (count($multipleStoresInWebsite) < 1) { + return []; + } + $connection = $this->moduleDataSetup->getConnection(); $batchSelectIterator = $this->batchQueryGenerator->generate( 'value_id', @@ -184,9 +199,10 @@ private function fetchAttributeValues($tableName) self::ATTRIBUTE_WEBSITE ) ->where( - 'cpei.store_id <> ?', - self::GLOBAL_STORE_VIEW_ID - ) + 'cpei.store_id IN (?)', + $multipleStoresInWebsite + ), + 1000 ); foreach ($batchSelectIterator as $select) { diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml index db148b2cf3114..f64812fd5bf49 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml @@ -157,7 +157,7 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}" stepKey="openCustomOptionsSection"/> + <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection"/> <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> <fillField userInput="option1" selector="{{AdminProductCustomizableOptionsSection.optionTitleInput}}" stepKey="fillOptionTitle"/> <click selector="{{AdminProductCustomizableOptionsSection.optionTypeOpenDropDown}}" stepKey="openTypeDropDown"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml index e8097cfa4fffb..6b47479d41cb7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/CustomOptionsActionGroup.xml @@ -6,12 +6,8 @@ */ --> -<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"> - - +<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"> <actionGroup name="CreateCustomRadioOptions"> - <!-- ActionGroup will add a single custom option to a product --> <!-- You must already be on the product creation page --> <arguments> @@ -39,8 +35,23 @@ <fillField stepKey="fillInValueTitle2" selector="{{AdminProductCustomizableOptionsSection.valueTitle}}" userInput="{{productOption2.title}}"/> <fillField stepKey="fillInValuePrice2" selector="{{AdminProductCustomizableOptionsSection.valuePrice}}" userInput="{{productOption2.price}}"/> + </actionGroup> - + <!--Add a custom option of type "file" to a product--> + <actionGroup name="AddProductCustomOptionFile"> + <arguments> + <argument name="option" defaultValue="ProductOptionFile"/> + </arguments> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" visible="false" stepKey="openCustomOptionSection"/> + <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" stepKey="waitForOption"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.lastOptionTitle}}" userInput="{{option.title}}" stepKey="fillTitle"/> + <click selector="{{AdminProductCustomizableOptionsSection.lastOptionTypeParent}}" stepKey="openTypeSelect"/> + <click selector="{{AdminProductCustomizableOptionsSection.optionType('File')}}" stepKey="selectTypeFile"/> + <waitForElementVisible selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" stepKey="waitForElements"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionPrice}}" userInput="{{option.price}}" stepKey="fillPrice"/> + <selectOption selector="{{AdminProductCustomizableOptionsSection.optionPriceType}}" userInput="{{option.price_type}}" stepKey="selectPriceType"/> + <fillField selector="{{AdminProductCustomizableOptionsSection.optionFileExtensions}}" userInput="{{option.file_extension}}" stepKey="fillCompatibleExtensions"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml index 05a2adf8ad7d1..25f059c84f4ef 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml @@ -8,6 +8,19 @@ <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"> + <!-- Go to storefront category product page by given parameters --> + <actionGroup name="GoToStorefrontCategoryPageByParameters"> + <arguments> + <argument name="category" type="string"/> + <argument name="mode" type="string"/> + <argument name="numOfProductsPerPage" type="string"/> + <argument name="sortBy" type="string" defaultValue="position"/> + </arguments> + <!-- Go to storefront category page --> + <amOnPage url="{{StorefrontCategoryPage.url(category)}}?product_list_limit={{numOfProductsPerPage}}&product_list_mode={{mode}}&product_list_order={{sortBy}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> + <!-- Check the category page --> <actionGroup name="StorefrontCheckCategoryActionGroup"> <arguments> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml index c8983b2090928..f67370dcff296 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml @@ -94,4 +94,25 @@ <data key="used_for_sort_by">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="productAttributeMultiselectTwoOptions" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">attribute</data> + <data key="frontend_input">multiselect</data> + <data key="scope">global</data> + <data key="is_required">false</data> + <data key="is_unique">false</data> + <data key="is_searchable">true</data> + <data key="is_visible">true</data> + <data key="is_visible_in_advanced_search">true</data> + <data key="is_visible_on_front">true</data> + <data key="is_filterable">true</data> + <data key="is_filterable_in_search">true</data> + <data key="used_in_product_listing">true</data> + <data key="is_used_for_promo_rules">true</data> + <data key="is_comparable">true</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <data key="used_for_sort_by">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml index 2fc451e6d40fd..15c2dc8bbebca 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml @@ -24,6 +24,38 @@ <requiredEntity type="StoreLabel">Option2Store0</requiredEntity> <requiredEntity type="StoreLabel">Option2Store1</requiredEntity> </entity> + <entity name="productAttributeOption3" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option3</data> + <data key="is_default">false</data> + <data key="sort_order">2</data> + <requiredEntity type="StoreLabel">Option3Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option3Store1</requiredEntity> + </entity> + <entity name="productAttributeOption4" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option4</data> + <data key="is_default">false</data> + <data key="sort_order">3</data> + <requiredEntity type="StoreLabel">Option4Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option4Store1</requiredEntity> + </entity> + <entity name="productAttributeOption5" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option5</data> + <data key="is_default">false</data> + <data key="sort_order">4</data> + <requiredEntity type="StoreLabel">Option5Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option5Store1</requiredEntity> + </entity> + <entity name="productAttributeOption6" type="ProductAttributeOption"> + <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> + <data key="label" unique="suffix">option6</data> + <data key="is_default">false</data> + <data key="sort_order">5</data> + <requiredEntity type="StoreLabel">Option6Store0</requiredEntity> + <requiredEntity type="StoreLabel">Option6Store1</requiredEntity> + </entity> <entity name="ProductAttributeOptionGetter" type="ProductAttributeOption"> <var key="attribute_code" entityKey="attribute_code" entityType="ProductAttribute"/> </entity> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 677214011c987..ccb5da7a6af6f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -119,6 +119,19 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ApiSimpleOneHidden" type="product2"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">1</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">123.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> <entity name="ApiSimpleTwo" type="product2"> <data key="sku" unique="suffix">api-simple-product-two</data> <data key="type_id">simple</data> @@ -132,6 +145,19 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ApiSimpleTwoHidden" type="product2"> + <data key="sku" unique="suffix">api-simple-product-two</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">1</data> + <data key="name" unique="suffix">Api Simple Product Two</data> + <data key="price">234.00</data> + <data key="urlKey" unique="suffix">api-simple-product-two</data> + <data key="status">1</data> + <data key="quantity">100</data> + <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> + <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> + </entity> <entity name="VirtualProduct" type="product"> <data key="sku" unique="suffix">virtualproduct</data> <data key="type_id">virtual</data> @@ -254,6 +280,10 @@ <requiredEntity type="product_option">ProductOptionDateTime</requiredEntity> <requiredEntity type="product_option">ProductOptionTime</requiredEntity> </entity> + <entity name="productWithOptions2" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionDropDownWithLongValuesTitle</requiredEntity> + </entity> <entity name="ApiVirtualProductWithDescription" type="product"> <data key="sku" unique="suffix">api-virtual-product</data> <data key="type_id">virtual</data> @@ -319,4 +349,39 @@ <data key="name" unique="suffix">Api Simple Product</data> <data key="price">100.00</data> </entity> + <entity name="ApiSimplePrice10Qty10" type="product"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">10.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStock10</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ApiSimplePrice100Qty100" type="product"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">100.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> + <entity name="ApiSimplePrice100Qty100v2" type="product"> + <data key="sku" unique="suffix">api-simple-product</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">Api Simple Product</data> + <data key="price">100.00</data> + <data key="urlKey" unique="suffix">api-simple-product</data> + <data key="status">1</data> + <requiredEntity type="product_extension_attribute">EavStock100</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml index 0f6b383c3b743..88ff2bbace47a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductExtensionAttributeData.xml @@ -11,4 +11,10 @@ <entity name="EavStockItem" type="product_extension_attribute"> <requiredEntity type="stock_item">Qty_1000</requiredEntity> </entity> + <entity name="EavStock100" type="product_extension_attribute"> + <requiredEntity type="stock_item">Qty_100</requiredEntity> + </entity> + <entity name="EavStock10" type="product_extension_attribute"> + <requiredEntity type="stock_item">Qty_10</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml index 95905eb90d926..903bf03535a37 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionData.xml @@ -59,6 +59,15 @@ <requiredEntity type="product_option_value">ProductOptionValueDropdown1</requiredEntity> <requiredEntity type="product_option_value">ProductOptionValueDropdown2</requiredEntity> </entity> + <entity name="ProductOptionDropDownWithLongValuesTitle" type="product_option"> + <var key="product_sku" entityType="product" entityKey="sku" /> + <data key="title">OptionDropDownWithLongTitles</data> + <data key="type">drop_down</data> + <data key="sort_order">4</data> + <data key="is_require">true</data> + <requiredEntity type="product_option_value">ProductOptionValueDropdownLongTitle1</requiredEntity> + <requiredEntity type="product_option_value">ProductOptionValueDropdownLongTitle2</requiredEntity> + </entity> <entity name="ProductOptionRadiobutton" type="product_option"> <var key="product_sku" entityType="product" entityKey="sku" /> <data key="title">OptionRadioButtons</data> @@ -112,4 +121,4 @@ <data key="price">0.00</data> <data key="price_type">percent</data> </entity> -</entities> \ No newline at end of file +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml index 815f8cf16809b..28dd255321844 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml @@ -50,4 +50,16 @@ <data key="price">2</data> <data key="price_type">fixed</data> </entity> -</entities> \ No newline at end of file + <entity name="ProductOptionValueDropdownLongTitle1" type="product_option_value"> + <data key="title">Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj11111</data> + <data key="sort_order">1</data> + <data key="price">10</data> + <data key="price_type">fixed</data> + </entity> + <entity name="ProductOptionValueDropdownLongTitle2" type="product_option_value"> + <data key="title">Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj22222Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj22222</data> + <data key="sort_order">2</data> + <data key="price">20</data> + <data key="price_type">percent</data> + </entity> +</entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml index 46a3fa3657f2c..4fae51de86c45 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StockItemData.xml @@ -12,4 +12,20 @@ <data key="qty">1000</data> <data key="is_in_stock">true</data> </entity> + <entity name="Qty_100" type="stock_item"> + <data key="qty">100</data> + <data key="is_in_stock">true</data> + </entity> + <entity name="Qty_10" type="stock_item"> + <data key="qty">10</data> + <data key="is_in_stock">true</data> + </entity> + <entity name="Qty_99" type="stock_item"> + <data key="qty">99</data> + <data key="is_in_stock">true</data> + </entity> + <entity name="Qty_101" type="stock_item"> + <data key="qty">101</data> + <data key="is_in_stock">true</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml index 097b388f45ea0..a703e56beda01 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/StoreLabelData.xml @@ -24,4 +24,36 @@ <data key="store_id">1</data> <data key="label">option2</data> </entity> + <entity name="Option3Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option3</data> + </entity> + <entity name="Option3Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option3</data> + </entity> + <entity name="Option4Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option4</data> + </entity> + <entity name="Option4Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option4</data> + </entity> + <entity name="Option5Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option5</data> + </entity> + <entity name="Option5Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option5</data> + </entity> + <entity name="Option6Store0" type="StoreLabel"> + <data key="store_id">0</data> + <data key="label">option6</data> + </entity> + <entity name="Option6Store1" type="StoreLabel"> + <data key="store_id">1</data> + <data key="label">option6</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml index 7723e0fe330cd..4541ad25af231 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminAddProductsToOptionPanelSection.xml @@ -13,6 +13,7 @@ <element name="filters" type="button" selector=".product_form_product_form_bundle-items_modal button[data-action='grid-filter-expand']" timeout="30"/> <element name="applyFilters" type="button" selector=".product_form_product_form_bundle-items_modal [data-action='grid-filter-apply']" timeout="30"/> <element name="nameFilter" type="input" selector=".product_form_product_form_bundle-items_modal input[name='name']"/> - <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> + <element name="firstCheckbox" type="input" selector="//tr[1]//input[@data-action='select-row']"/> + <element name="nthCheckbox" type="input" selector="//tr[{{var}}]//input[@data-action='select-row']" parameterized="true"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml index 8a6b5bf5eb842..d991580d58124 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductCustomizableOptionsSection.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> <section name="AdminProductCustomizableOptionsSection"> <element name="checkIfCustomizableOptionsTabOpen" type="text" selector="//span[text()='Customizable Options']/parent::strong/parent::*[@data-state-collapsible='closed']"/> - <element name="customezableOptions" type="text" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Customizable Options']"/> + <element name="customizableOptions" type="text" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Customizable Options']"/> <element name="useDefaultOptionTitle" type="text" selector="[data-index='options'] tr.data-row [data-index='title'] [name^='options_use_default']"/> <element name="useDefaultOptionTitleByIndex" type="text" selector="[data-index='options'] [data-index='values'] tr[data-repeat-index='{{var1}}'] [name^='options_use_default']" parameterized="true"/> <element name="addOptionBtn" type="button" selector="button[data-index='button_add']"/> @@ -20,7 +20,6 @@ <element name="optionTypeTextField" type="button" selector=".admin__dynamic-rows[data-index='options'] .action-menu._active li li"/> <element name="maxCharactersInput" type="input" selector="input[name='product[options][0][max_characters]']"/> - <element name="checkSelect" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//span[text()='Option Type']/parent::label/parent::div//div[@data-role='selected-option']" parameterized="true"/> <element name="checkDropDown" type="select" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//parent::label/parent::div//li[@class='admin__action-multiselect-menu-inner-item']//label[text()='Drop-down']" parameterized="true"/> <element name="clickAddValue" type="button" selector="//span[text()='{{var1}}']/parent::div/parent::div/parent::div//tfoot//button" parameterized="true"/> @@ -38,5 +37,10 @@ <element name="addValue" type="button" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@data-action='add_new_row']" /> <element name="valueTitle" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__control-table')]//tbody/tr[last()]//*[@data-index='title']//input" /> <element name="valuePrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[contains(@class, 'admin__control-table')]//tbody/tr[last()]//*[@data-index='price']//input" /> + + <element name="optionPrice" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price]']"/> + <element name="optionPriceType" type="select" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][price_type]']"/> + <element name="optionSku" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][sku]']"/> + <element name="optionFileExtensions" type="input" selector="//*[@data-index='custom_options']//*[@data-index='options']/tbody/tr[last()]//*[@name='product[options][0][file_extension]']"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml index ad6b5feb4e09e..a314f9cd0ddfd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormActionSection.xml @@ -12,8 +12,7 @@ <element name="backButton" type="button" selector="#back" timeout="30"/> <element name="saveButton" type="button" selector="#save-button" timeout="30"/> <element name="saveArrow" type="button" selector="button[data-ui-id='save-button-dropdown']" timeout="30"/> - <!--<element name="saveAndClose" type="button" selector="span[title='Save & Close']" timeout="30"/>--> - <element name="saveAndClose" type="button" selector="span[id='save_and_close']" timeout="30"/> + <element name="saveAndClose" type="button" selector="span[title='Save & Close']" timeout="30"/> <element name="changeStoreButton" type="button" selector="#store-change-button" timeout="10"/> <element name="selectStoreView" type="button" selector="//ul[@data-role='stores-list']/li/a[normalize-space(.)='{{var1}}']" timeout="10" parameterized="true"/> </section> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml index a60e3bb05cc92..2a6003d837b2e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml @@ -24,5 +24,7 @@ <element name="productLinkByHref" type="text" selector="a.product-item-link[href$='{{var1}}.html']" parameterized="true"/> <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> <element name="categoryImage" type="text" selector=".category-image"/> + <element name="emptyProductMessage" type="block" selector=".message.info.empty>div"/> + <element name="lineProductName" type="text" selector=".products.list.items.product-items li:nth-of-type({{line}}) .product-item-link" timeout="30" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml index 923414e60cf5f..4892e25818e2c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml @@ -11,14 +11,19 @@ <section name="StorefrontCategoryProductSection"> <element name="ProductTitleByNumber" type="button" selector="//main//li[{{var1}}]//a[@class='product-item-link']" parameterized="true"/> <element name="ProductPriceByNumber" type="text" selector="//main//li[{{var1}}]//span[@class='price']" parameterized="true"/> + <element name="ProductSpecialPriceByNumber" type="text" selector="//main//li[{{var1}}]//span[@class='special-price']//span[@class='price']" parameterized="true"/> + <element name="ProductOldPriceByNumber" type="text" selector="//main//li[{{var1}}]//span[@class='old-price']//span[@class='price']" parameterized="true"/> <element name="ProductInfoByNumber" type="text" selector="//main//li[{{var1}}]//div[@class='product-item-info']" parameterized="true"/> <element name="ProductAddToCompareByNumber" type="text" selector="//main//li[{{var1}}]//a[contains(@class, 'tocompare')]" parameterized="true"/> + <element name="listedProduct" type="block" selector="ol li:nth-child({{productPositionInList}}) img" parameterized="true"/> + <element name="categoryListView" type="button" selector="a[title='List']" timeout="30"/> <element name="ProductTitleByName" type="button" selector="//main//li//a[contains(text(), '{{var1}}')]" parameterized="true"/> <element name="ProductPriceByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//span[@class='price']" parameterized="true"/> <element name="ProductImageByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[@class='product-image-photo']" parameterized="true"/> <element name="ProductImageBySrc" type="text" selector=".products-grid img[src*='{{pattern}}']" parameterized="true"/> <element name="ProductInfoByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//div[@class='product-item-info']" parameterized="true"/> + <element name="ProductAddToCartByName" type="button" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocart')]" parameterized="true"/> <element name="ProductAddToCompareByName" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//a[contains(@class, 'tocompare')]" parameterized="true"/> <element name="ProductImageByNameAndSrc" type="text" selector="//main//li[.//a[contains(text(), '{{var1}}')]]//img[contains(@src, '{{src}}')]" parameterized="true"/> </section> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryTopToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryTopToolbarSection.xml new file mode 100644 index 0000000000000..2b44bf1db7efd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryTopToolbarSection.xml @@ -0,0 +1,16 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontCategoryTopToolbarSection"> + <element name="gridMode" type="button" selector=".//*[@class='toolbar toolbar-products'][1]//*[@id='mode-grid']" timeout="30"/> + <element name="listMode" type="button" selector=".//*[@class='toolbar toolbar-products'][1]//*[@id='mode-list']" timeout="30"/> + <element name="sortByDropdown" type="select" selector=".//*[@class='toolbar toolbar-products'][1]//*[@id='sorter']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index f0ca8cb74e3e1..7e1bec48aeebc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -15,7 +15,10 @@ <element name="productPriceLabel" type="text" selector=".price-label"/> <element name="productPrice" type="text" selector="div.price-box.price-final_price"/> <element name="specialPrice" type="text" selector=".special-price"/> + <element name="updatedPrice" type="text" selector="div.price-box.price-final_price [data-price-type='finalPrice'] .price"/> <element name="oldPrice" type="text" selector=".old-price"/> + <element name="oldPriceTag" type="text" selector=".old-price .price-label"/> + <element name="oldPriceAmount" type="text" selector=".old-price .price"/> <element name="productStockStatus" type="text" selector=".stock[title=Availability]>span"/> <element name="productImage" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[@class='fotorama__img']"/> <element name="productImageSrc" type="text" selector="//*[@id='maincontent']//div[@class='gallery-placeholder']//img[contains(@src, '{{src}}')]" parameterized="true"/> @@ -31,7 +34,7 @@ <!-- The 1st parameter is the nth custom option, the 2nd parameter is the nth value in the option --> <element name="nthCustomOptionInput" type="radio" selector="//*[@id='product-options-wrapper']/*[@class='fieldset']/*[contains(@class, 'field')][{{customOptionNum}}]//*[contains(@class, 'admin__field-option')][{{customOptionValueNum}}]//input" parameterized="true" /> <element name="productOptionRadioButtonsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{var1}}')]/../div[@class='control']//input[@price='{{var2}}']" parameterized="true"/> - + <element name="productOptionDataMonth" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='month']" parameterized="true"/> <element name="productOptionDataDay" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='day']" parameterized="true"/> <element name="productOptionDataYear" type="date" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//legend[contains(.,'{{var1}}')]/../div[@class='control']//select[@data-calendar-role='year']" parameterized="true"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml index 632ee2dea68eb..acda5c40af8a3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -10,8 +10,10 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> <section name="StorefrontProductPageSection"> <element name="qtyInput" type="button" selector="input.input-text.qty"/> - <element name="addToCartBtn" type="button" selector="button.action.tocart.primary"/> + <element name="addToCartBtn" type="button" selector="button.action.tocart.primary" timeout="30"/> <element name="successMsg" type="button" selector="div.message-success"/> + <element name="alertMessage" type="text" selector=".page.messages [role=alert]"/> + <element name="messagesBlock" type="text" selector=".page.messages"/> <element name="addToWishlist" type="button" selector="//a[@class='action towishlist']" timeout="30"/> <element name="customTextOptionInput" type="input" selector=".input-text.product-custom-option"/> <element name="charCounter" type="text" selector=".character-counter"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml index 43caafe8790d3..012c956c2dbef 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> <test name="AdminMultipleWebsitesUseDefaultValuesTest"> <annotations> + <features value="Catalog"/> <title value="Use Default Value checkboxes should be checked for new website scope"/> <description value="Use Default Value checkboxes for product attribute should be checked for new website scope"/> <severity value="MAJOR"/> @@ -18,7 +19,7 @@ </annotations> <after> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="Second Website"/> </actionGroup> <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> </after> @@ -35,9 +36,10 @@ <!--Create Store view --> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="clickCreateStoreViewButton"/> + <waitForPageLoad stepKey="waitForSystemStorePage"/> + <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> - <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitFornewStorePageToOpen" after="clickCreateStoreViewButton"/> + <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> @@ -50,6 +52,7 @@ <!--Create a Simple Product --> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> + <waitForPageLoad stepKey="waitForProductGrid"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillProductName"/> @@ -68,7 +71,6 @@ <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> - <!--<waitForPageLoad stepKey="waitForStoreViewSwitched"/>--> <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml index 0166d15e226c0..23d9a2a9e4cd0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml @@ -34,6 +34,7 @@ <!--Create Store view --> <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> + <waitForElementVisible selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="waitForStoreViewBtn"/> <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> @@ -64,8 +65,8 @@ <fillField userInput="{{_defaultProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - <!--<click selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}" stepKey="openCustomOptionsSection"/>--> - <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> + <!--<click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection"/>--> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="clickAddOption"/> <waitForPageLoad stepKey="waitAfterAddOption"/> <fillField selector="input[name='product[options][0][title]']" userInput="Radio Option" stepKey="fillOptionTitle"/> @@ -97,7 +98,7 @@ <waitForLoadingMaskToDisappear stepKey="waitForProductPagetoSaveAgain"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> - <click selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}" stepKey="openCustomOptionsSection2"/> + <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection2"/> <seeNumberOfElements selector=".admin__dynamic-rows[data-index='values'] tr.data-row" userInput="3" stepKey="see4RowsOfOptions"/> </test> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml index e05a259eaf974..084ae62788d41 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> <test name="SimpleProductTwoCustomOptionsTest"> <annotations> <features value="Catalog"/> @@ -41,7 +41,7 @@ </after> <!-- opens the custom option panel and clicks add options --> - <click stepKey="openCustomizableOptions" selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}"/> + <click stepKey="openCustomizableOptions" selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}"/> <waitForPageLoad stepKey="waitForCustomOptionsOpen"/> <!-- Create a custom option with 2 values --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml index a8bb56633cfb9..7d843012d1d1d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViews.xml @@ -77,7 +77,7 @@ <!-- Update Product with Option Value DropDown 1--> - <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses2"/> <click selector="{{AdminProductCustomizableOptionsSection.addOptionBtn}}" stepKey="checkAddOption1"/> <waitForPageLoad time="10" stepKey="waitForPageLoad7"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionTitle('New Option')}}" userInput="Custom Options 1" stepKey="fillOptionTitle1"/> @@ -104,7 +104,7 @@ <!-- Open tab Customizable Options --> <waitForPageLoad time="10" stepKey="waitForPageLoad2"/> - <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses3"/> <!-- Update Option Customizable Options and Option Value 1--> @@ -124,6 +124,7 @@ <!-- Login Customer Storefront --> <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> + <waitForPageLoad stepKey="waitForSignInPage"/> <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> @@ -131,7 +132,7 @@ <!-- Go to Product Page --> <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct1Page"/> - + <waitForPageLoad stepKey="waitForProductPage"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeproductOptionDropDownOptionTitle1"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeproductOptionDropDownOptionTitle2"/> @@ -194,13 +195,14 @@ <!-- Switch to FR Store View Storefront --> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> + <waitForPageLoad stepKey="waitForStorefrontHomePage"/> <click selector="{{StorefrontHeaderSection.storeViewSwitcher}}" stepKey="clickStoreViewSwitcher1"/> <waitForElementVisible selector="{{StorefrontHeaderSection.storeViewDropdown}}" stepKey="waitForStoreViewDropdown1"/> <click selector="{{StorefrontHeaderSection.storeViewOption(customStoreFR.code)}}" stepKey="selectStoreView1"/> <waitForPageLoad stepKey="waitForPageLoad4"/> <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page"/> - + <waitForPageLoad stepKey="waitForProductPage2"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('FR Custom Options 1')}}" stepKey="seeProductFrOptionDropDownTitle"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option1')}}" stepKey="productFrOptionDropDownOptionTitle1"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('FR Custom Options 1', 'FR option2')}}" stepKey="productFrOptionDropDownOptionTitle2"/> @@ -264,7 +266,7 @@ <!-- Open tab Customizable Options --> <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> - <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customezableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses4" /> + <conditionalClick selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" dependentSelector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" visible="true" stepKey="clickIfContentTabCloses4" /> <!-- Update Option Customizable Options and Option Value 1--> @@ -281,7 +283,7 @@ <!--Go to Product Page--> <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProduct2Page2"/> - + <waitForPageLoad stepKey="waitForProductPage3"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownTitle('Custom Options 1')}}" stepKey="seeProductOptionDropDownTitle1"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option1')}}" stepKey="seeProductOptionDropDownOptionTitle3"/> <seeElement selector="{{StorefrontProductInfoMainSection.productOptionDropDownOptionTitle('Custom Options 1', 'option2')}}" stepKey="seeProductOptionDropDownOptionTitle4"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml new file mode 100644 index 0000000000000..9e0f80eb9ea5d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle.xml @@ -0,0 +1,106 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitle"> + <annotations> + <group value="Catalog"/> + <title value="Admin should be able to see the full title of the selected custom option value in the order"/> + <description value="Admin should be able to see the full title of the selected custom option value in the order"/> + <severity value="MAJOR"/> + <testCaseId value="MC-3043"/> + <group value="skip"/> + <!-- Skip due to MQE-1128 --> + </annotations> + <before> + <!--Create Simple Product with Custom Options--> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">17</field> + </createData> + <updateData createDataKey="createProduct" entity="productWithOptions2" stepKey="updateProductWithOptions"/> + + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Login Customer Storefront --> + + <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> + <fillField userInput="$$createCustomer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> + <fillField userInput="$$createCustomer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> + <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> + + <!-- Checking the correctness of displayed prices for user parameters --> + + <amOnPage url="{{StorefrontHomePage.url}}$$createProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage"/> + <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptionsDropDown(ProductOptionDropDownWithLongValuesTitle.title, ProductOptionValueDropdownLongTitle1.price)}}" stepKey="checkDropDownProductOption"/> + + <!-- Adding items to the checkout --> + + <selectOption userInput="{{ProductOptionValueDropdownLongTitle1.price}}" selector="{{StorefrontProductInfoMainSection.productOptionSelect(ProductOptionDropDownWithLongValuesTitle.title)}}" stepKey="seeProductOptionDropDown"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.productPrice}}" stepKey="finalProductPrice"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + + <!-- Checking the correctness of displayed custom options for user parameters on checkout --> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart" /> + + <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsArea}}" visible="true" stepKey="exposeMiniCart"/> + + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskForCartItem"/> + <waitForElement selector="{{CheckoutPaymentSection.cartItemsAreaActive}}" time="30" stepKey="waitForCartItemsAreaActive"/> + + <see selector="{{CheckoutPaymentSection.cartItems}}" userInput="$$createProduct.name$$" stepKey="seeProductInCart"/> + + <conditionalClick selector="{{CheckoutPaymentSection.ProductOptionsByProductItemName($$createProduct.name$$)}}" dependentSelector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" visible="false" stepKey="exposeProductOptions"/> + + <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="seeProductOptionValueDropdown1Input1"/> + + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + + <!-- Place Order --> + + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Login to Admin and open Order --> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnOrdersPage"/> + <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="fillOrderNum"/> + <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearchOrderNum"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearOnSearch"/> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + + <!-- Checking the correctness of displayed custom options for user parameters on Order --> + + <dontSee selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="dontSeeAdminOrderProductOptionValueDropdown1"/> + <grabTextFrom selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd" stepKey="productOptionValueText"/> + <assertEquals stepKey="checkProductOptionValue"> + <actualResult type="variable">productOptionValueText</actualResult> + <expectedResult type="string">Optisfvdklvfnkljvnfdklpvnfdjklfdvnjkvfdkjnvfdjkfvndj111 ...</expectedResult> + </assertEquals> + <moveMouseOver selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd" stepKey="hoverProduct"/> + <waitForElementVisible selector="{{AdminOrderItemsOrderedSection.productNameOptions}} dd:nth-child(2)" stepKey="waitForCustomOptionValueFullName"/> + <see selector="{{AdminOrderItemsOrderedSection.productNameOptions}}" userInput="{{ProductOptionValueDropdownLongTitle1.title}}" stepKey="seeAdminOrderProductOptionValueDropdown1"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php index ba716fdb53c89..47a60a1916142 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/ShowUpdateResultTest.php @@ -58,7 +58,7 @@ protected function getContext() $objectManagerMock = $this->getMockForAbstractClass(\Magento\Framework\ObjectManagerInterface::class); $objectManagerMock->expects($this->any()) ->method('get') - ->willreturn($productActionMock); + ->willReturn($productActionMock); $eventManager = $this->getMockBuilder(\Magento\Framework\Event\Manager::class) ->setMethods(['dispatch']) diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml index 8e5583a6699b7..30c05c2ec689b 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml @@ -13,7 +13,7 @@ <div class="admin__field field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> <label class="label admin__field-label"> <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> </label> <div class="admin__field-control control"> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml index edf4f68afded7..4ad7a95c91980 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml @@ -7,6 +7,7 @@ // @codingStandardsIgnoreFile ?> +<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> <?php $_option = $block->getOption(); ?> <?php $_fileInfo = $block->getFileInfo(); ?> <?php $_fileExists = $_fileInfo->hasData() ? true : false; ?> @@ -64,7 +65,7 @@ require(['prototype'], function(){ <div class="admin__field <?php if ($_option->getIsRequire()) echo ' required _required' ?>"> <label class="admin__field-label label"> <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> </label> <div class="admin__field-control control"> <?php if ($_fileExists): ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml index 14e485c6445e0..11fba22ea8139 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/text.phtml @@ -12,7 +12,7 @@ <div class="field admin__field<?php if ($_option->getIsRequire()) echo ' required _required' ?>"> <label class="admin__field-label label"> <?= $block->escapeHtml($_option->getTitle()) ?> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> </label> <div class="control admin__field-control"> <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_FIELD): ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml index 3420512977aad..66895fa1eabf9 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/date.phtml @@ -7,6 +7,7 @@ // @codingStandardsIgnoreFile ?> +<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> <?php $_option = $block->getOption() ?> <?php $_optionId = $_option->getId() ?> <?php $class = ($_option->getIsRequire()) ? ' required' : ''; ?> @@ -15,7 +16,7 @@ <fieldset class="fieldset fieldset-product-options-inner<?= /* @escapeNotVerified */ $class ?>"> <legend class="legend"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> </legend> <div class="control"> <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml index 3ceba2eebd214..adb729c6d86ec 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml @@ -7,6 +7,7 @@ // @codingStandardsIgnoreFile ?> +<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> <?php $_option = $block->getOption(); ?> <?php $_fileInfo = $block->getFileInfo(); ?> <?php $_fileExists = $_fileInfo->hasData(); ?> @@ -19,7 +20,7 @@ <div class="field file<?= /* @escapeNotVerified */ $class ?>"> <label class="label" for="<?= /* @noEscape */ $_fileName ?>" id="<?= /* @noEscape */ $_fileName ?>-label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> </label> <?php if ($_fileExists): ?> <div class="control"> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml index 852e0095f2f66..a04e366a43a2d 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/text.phtml @@ -7,6 +7,7 @@ // @codingStandardsIgnoreFile ?> +<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Text */ ?> <?php $_option = $block->getOption(); $class = ($_option->getIsRequire()) ? ' required' : ''; @@ -17,7 +18,7 @@ $class = ($_option->getIsRequire()) ? ' required' : ''; } ?><?= /* @escapeNotVerified */ $class ?>"> <label class="label" for="options_<?= /* @escapeNotVerified */ $_option->getId() ?>_text"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> - <?= /* @escapeNotVerified */ $block->getFormatedPrice() ?> + <?= /* @escapeNotVerified */ $block->getFormattedPrice() ?> </label> <div class="control"> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js index 8fcac2f9f1d65..83e91d3c3d4c7 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/catalog-add-to-cart.js @@ -49,6 +49,23 @@ define([ }); }, + /** + * @private + */ + _redirect: function (url) { + var urlParts, locationParts, forceReload; + + urlParts = url.split('#'); + locationParts = window.location.href.split('#'); + forceReload = urlParts[0] === locationParts[0]; + + window.location.assign(url); + + if (forceReload) { + window.location.reload(); + } + }, + /** * @return {Boolean} */ @@ -62,34 +79,28 @@ define([ * @param {Object} form */ submitForm: function (form) { - var addToCartButton, self = this; - - if (form.has('input[type="file"]').length && form.find('input[type="file"]').val() !== '') { - self.element.off('submit'); - // disable 'Add to Cart' button - addToCartButton = $(form).find(this.options.addToCartButtonSelector); - addToCartButton.prop('disabled', true); - addToCartButton.addClass(this.options.addToCartButtonDisabledClass); - form.submit(); - } else { - self.ajaxSubmit(form); - } + this.ajaxSubmit(form); }, /** * @param {String} form */ ajaxSubmit: function (form) { - var self = this; + var self = this, + formData; $(self.options.minicartSelector).trigger('contentLoading'); self.disableAddToCartButton(form); + formData = new FormData(form[0]); $.ajax({ url: form.attr('action'), - data: form.serialize(), + data: formData, type: 'post', dataType: 'json', + cache: false, + contentType: false, + processData: false, /** @inheritdoc */ beforeSend: function () { @@ -125,7 +136,8 @@ define([ parameters.push(eventData.redirectParameters.join('&')); res.backUrl = parameters.join('#'); } - window.location = res.backUrl; + + self._redirect(res.backUrl); return; } diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index a1f581743a645..ebd8671de02fb 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -28,6 +28,10 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection) : voi /** @var FieldNode $field */ foreach ($query as $field) { + if ($field->kind === 'InlineFragment') { + continue; + } + if (!$collection->isAttributeAdded($field->name->value)) { $collection->addAttributeToSelect($field->name->value); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index baa456c7821ed..dbe58a9c77cd0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -26,6 +26,10 @@ public function calculate(FieldNode $fieldNode) : int $depth = count($selections) ? 1 : 0; $childrenDepth = [0]; foreach ($selections as $node) { + if ($node->kind === 'InlineFragment') { + continue; + } + $childrenDepth[] = $this->calculate($node); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php index eb57873850b80..0401e1c42331e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/LevelCalculator.php @@ -37,7 +37,7 @@ public function calculate(int $rootCategoryId) : int { $connection = $this->resourceConnection->getConnection(); $select = $connection->select() - ->from($connection->getTableName('catalog_category_entity'), 'level') + ->from($this->resourceConnection->getTableName('catalog_category_entity'), 'level') ->where($this->resourceCategory->getLinkField() . " = ?", $rootCategoryId); return (int) $connection->fetchOne($select); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php index 3c01579410638..da457279728bb 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php @@ -24,9 +24,9 @@ class CategoryTree { /** - * In depth we need to calculate only children nodes, so 2 first wrapped nodes should be ignored + * In depth we need to calculate only children nodes, so the first wrapped node should be ignored */ - const DEPTH_OFFSET = 2; + const DEPTH_OFFSET = 1; /** * @var CollectionFactory @@ -143,6 +143,10 @@ private function joinAttributesRecursively(Collection $collection, FieldNode $fi /** @var FieldNode $node */ foreach ($subSelection as $node) { + if ($node->kind === 'InlineFragment') { + continue; + } + $this->joinAttributesRecursively($collection, $node); } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php index 5de9d3880b5d2..db5b07279ed1c 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php @@ -75,6 +75,7 @@ protected function initCategories() $collection->addAttributeToSelect('name') ->addAttributeToSelect('url_key') ->addAttributeToSelect('url_path'); + $collection->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID); /* @var $collection \Magento\Catalog\Model\ResourceModel\Category\Collection */ foreach ($collection as $category) { $structure = explode(self::DELIMITER_CATEGORY, $category->getPath()); diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index dd33b94423696..afd018f077d20 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -28,6 +28,13 @@ abstract class AbstractType */ public static $commonAttributesCache = []; + /** + * Maintain a list of invisible attributes + * + * @var array + */ + public static $invAttributesCache = []; + /** * Attribute Code to Id cache * @@ -278,7 +285,14 @@ protected function _initAttributes() } } foreach ($absentKeys as $attributeSetName => $attributeIds) { - $this->attachAttributesById($attributeSetName, $attributeIds); + $unknownAttributeIds = array_diff( + $attributeIds, + array_keys(self::$commonAttributesCache), + self::$invAttributesCache + ); + if ($unknownAttributeIds || $this->_forcedAttributesCodes) { + $this->attachAttributesById($attributeSetName, $attributeIds); + } } foreach ($entityAttributes as $attributeRow) { if (isset(self::$commonAttributesCache[$attributeRow['attribute_id']])) { @@ -303,37 +317,45 @@ protected function _initAttributes() protected function attachAttributesById($attributeSetName, $attributeIds) { foreach ($this->_prodAttrColFac->create()->addFieldToFilter( - 'main_table.attribute_id', - ['in' => $attributeIds] + ['main_table.attribute_id', 'main_table.attribute_code'], + [ + ['in' => $attributeIds], + ['in' => $this->_forcedAttributesCodes] + ] ) as $attribute) { $attributeCode = $attribute->getAttributeCode(); $attributeId = $attribute->getId(); if ($attribute->getIsVisible() || in_array($attributeCode, $this->_forcedAttributesCodes)) { - self::$commonAttributesCache[$attributeId] = [ - 'id' => $attributeId, - 'code' => $attributeCode, - 'is_global' => $attribute->getIsGlobal(), - 'is_required' => $attribute->getIsRequired(), - 'is_unique' => $attribute->getIsUnique(), - 'frontend_label' => $attribute->getFrontendLabel(), - 'is_static' => $attribute->isStatic(), - 'apply_to' => $attribute->getApplyTo(), - 'type' => \Magento\ImportExport\Model\Import::getAttributeType($attribute), - 'default_value' => strlen( - $attribute->getDefaultValue() - ) ? $attribute->getDefaultValue() : null, - 'options' => $this->_entityModel->getAttributeOptions( - $attribute, - $this->_indexValueAttributes - ), - ]; + if (!isset(self::$commonAttributesCache[$attributeId])) { + self::$commonAttributesCache[$attributeId] = [ + 'id' => $attributeId, + 'code' => $attributeCode, + 'is_global' => $attribute->getIsGlobal(), + 'is_required' => $attribute->getIsRequired(), + 'is_unique' => $attribute->getIsUnique(), + 'frontend_label' => $attribute->getFrontendLabel(), + 'is_static' => $attribute->isStatic(), + 'apply_to' => $attribute->getApplyTo(), + 'type' => \Magento\ImportExport\Model\Import::getAttributeType($attribute), + 'default_value' => strlen( + $attribute->getDefaultValue() + ) ? $attribute->getDefaultValue() : null, + 'options' => $this->_entityModel->getAttributeOptions( + $attribute, + $this->_indexValueAttributes + ), + ]; + } + self::$attributeCodeToId[$attributeCode] = $attributeId; $this->_addAttributeParams( $attributeSetName, self::$commonAttributesCache[$attributeId], $attribute ); + } else { + self::$invAttributesCache[] = $attributeId; } } } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 0939acabbd5fd..e7ffe408cc732 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -170,14 +170,25 @@ public function move($fileName, $renameFileOff = false) } } + if ($this->getTmpDir()) { + $filePath = $this->getTmpDir() . '/'; + } else { + $filePath = ''; + } $fileName = preg_replace('/[^a-z0-9\._-]+/i', '', $fileName); + $filePath = $this->_directory->getRelativePath($filePath . $fileName); $this->_directory->writeFile( - $this->_directory->getRelativePath($this->getTmpDir() . '/' . $fileName), + $filePath, $read->readAll() ); } - $filePath = $this->_directory->getRelativePath($this->getTmpDir() . '/' . $fileName); + if ($this->getTmpDir()) { + $filePath = $this->getTmpDir() . '/'; + } else { + $filePath = ''; + } + $filePath = $this->_directory->getRelativePath($filePath . $fileName); $this->_setUploadFile($filePath); $destDir = $this->_directory->getAbsolutePath($this->getDestDir()); $result = $this->save($destDir); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php index 70ac3a4fa2e97..bd2fe896b8c0a 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php @@ -134,8 +134,28 @@ protected function setUp() ->expects($this->any()) ->method('addFieldToFilter') ->with( - 'main_table.attribute_id', - ['in' => ['attribute_id', 'boolean_attribute']] + ['main_table.attribute_id', 'main_table.attribute_code'], + [ + [ + 'in' => + [ + 'attribute_id', + 'boolean_attribute', + ], + ], + [ + 'in' => + [ + 'related_tgtr_position_behavior', + 'related_tgtr_position_limit', + 'upsell_tgtr_position_behavior', + 'upsell_tgtr_position_limit', + 'thumbnail_label', + 'small_image_label', + 'image_label', + ], + ], + ] ) ->willReturn([$attribute1, $attribute2]); diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php index ed95d5a0212c9..262593377aa2c 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php @@ -103,7 +103,7 @@ protected function setUp() public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName) { $destDir = 'var/dest/dir'; - $expectedRelativeFilePath = $this->uploader->getTmpDir() . '/' . $expectedFileName; + $expectedRelativeFilePath = $expectedFileName; $this->directoryMock->expects($this->once())->method('isWritable')->with($destDir)->willReturn(true); $this->directoryMock->expects($this->any())->method('getRelativePath')->with($expectedRelativeFilePath); $this->directoryMock->expects($this->once())->method('getAbsolutePath')->with($destDir) @@ -139,7 +139,7 @@ public function testMoveFileName() { $destDir = 'var/dest/dir'; $fileName = 'test_uploader_file'; - $expectedRelativeFilePath = $this->uploader->getTmpDir() . '/' . $fileName; + $expectedRelativeFilePath = $fileName; $this->directoryMock->expects($this->once())->method('isWritable')->with($destDir)->willReturn(true); $this->directoryMock->expects($this->any())->method('getRelativePath')->with($expectedRelativeFilePath); $this->directoryMock->expects($this->once())->method('getAbsolutePath')->with($destDir) diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml new file mode 100644 index 0000000000000..ad92af9bb3b1b --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml @@ -0,0 +1,44 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <!-- action group to create a new catalog price rule giving a catalogRule entity --> + <actionGroup name="newCatalogPriceRuleByUI"> + <arguments> + <argument name="catalogRule" defaultValue="_defaultCatalogRule"/> + </arguments> + <!-- Go to the admin Catalog rule grid and add a new one --> + <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <waitForPageLoad stepKey="waitForPriceRulePage"/> + <click stepKey="addNewRule" selector="{{AdminGridMainControls.add}}"/> + + <!-- Fill the form according the the attributes of the entity --> + <fillField stepKey="fillName" selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{catalogRule.name}}"/> + <fillField stepKey="fillDescription" selector="{{AdminNewCatalogPriceRule.description}}" userInput="{{catalogRule.description}}"/> + <selectOption stepKey="selectSite" selector="{{AdminNewCatalogPriceRule.websites}}" userInput="{{catalogRule.website_ids[0]}}"/> + <selectOption stepKey="selectCustomerGroups" selector="{{AdminNewCatalogPriceRule.customerGroups}}" userInput="{{catalogRule.customer_group_ids[0]}}"/> + <click stepKey="clickFromCalender" selector="{{AdminNewCatalogPriceRule.fromDateButton}}"/> + <click stepKey="clickFromToday" selector="{{AdminNewCatalogPriceRule.todayDate}}"/> + <click stepKey="clickToCalender" selector="{{AdminNewCatalogPriceRule.toDateButton}}"/> + <click stepKey="clickToToday" selector="{{AdminNewCatalogPriceRule.todayDate}}"/> + <click stepKey="openActionDropdown" selector="{{AdminNewCatalogPriceRule.actionsTab}}"/> + <selectOption stepKey="discountType" selector="{{AdminNewCatalogPriceRuleActions.apply}}" userInput="{{catalogRule.simple_action}}"/> + <fillField stepKey="fillDiscountValue" selector="{{AdminNewCatalogPriceRuleActions.discountAmount}}" userInput="{{catalogRule.discount_amount}}"/> + + <!-- Scroll to top and either save or save and apply after the action group --> + <scrollToTopOfPage stepKey="scrollToTop"/> + </actionGroup> + + <actionGroup name="applyCatalogPriceRules"> + <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <waitForPageLoad stepKey="waitForPriceRulePage"/> + <click stepKey="applyRules" selector="{{AdminCatalogPriceRuleGrid.applyRules}}"/> + <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="Updated rules applied."/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml index 2b11a4db0578d..3199a53026a92 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml @@ -21,4 +21,46 @@ <data key="simple_action">by_percent</data> <data key="discount_amount">10</data> </entity> + + <entity name="CatalogRuleByFixed" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">by_fixed</data> + <data key="discount_amount">10</data> + </entity> + + <entity name="CatalogRuleToPercent" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">to_percent</data> + <data key="discount_amount">90</data> + </entity> + + <entity name="CatalogRuleToFixed" type="catalogRule"> + <data key="name" unique="suffix">CatalogPriceRule</data> + <data key="description">Catalog Price Rule Description</data> + <data key="is_active">1</data> + <array key="customer_group_ids"> + <item>0</item> + </array> + <array key="website_ids"> + <item>1</item> + </array> + <data key="simple_action">to_fixed</data> + <data key="discount_amount">100</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRulePage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRulePage.xml new file mode 100644 index 0000000000000..e080a252e7855 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/CatalogRulePage.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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> + <page name="CatalogRulePage" url="catalog_rule/promo_catalog/" module="Magento_CatalogRule" area="admin"> + <section name="AdminSecondaryGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml new file mode 100644 index 0000000000000..b60a4c6516a82 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml @@ -0,0 +1,42 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewCatalogPriceRule"> + <element name="saveAndApply" type="button" selector="#save_and_apply" timeout="30"/> + <element name="save" type="button" selector="#save" timeout="30"/> + <element name="delete" type="button" selector="#delete" timeout="30"/> + + <element name="ruleName" type="input" selector="[name='name']"/> + <element name="description" type="textarea" selector="[name='description']"/> + <element name="status" type="select" selector="[name='is_active']"/> + + <element name="websites" type="select" selector="[name='website_ids']"/> + <element name="websitesOptions" type="select" selector="[name='website_ids'] option"/> + <element name="customerGroups" type="select" selector="[name='customer_group_ids']"/> + <element name="customerGroupsOptions" type="select" selector="[name='customer_group_ids'] option"/> + + <element name="fromDateButton" type="button" selector="[name='from_date'] + button" timeout="15"/> + <element name="toDateButton" type="button" selector="[name='to_date'] + button" timeout="15"/> + <element name="todayDate" type="button" selector="#ui-datepicker-div [data-handler='today']"/> + <element name="priority" type="input" selector="[name='sort_order']"/> + <element name="conditionsTab" type="block" selector="[data-index='block_promo_catalog_edit_tab_conditions']"/> + <element name="actionsTab" type="block" selector="[data-index='actions']"/> + </section> + + <section name="AdminNewCatalogPriceRuleActions"> + <element name="apply" type="select" selector="[name='simple_action']"/> + <element name="discountAmount" type="input" selector="[name='discount_amount']"/> + <element name="disgardRules" type="select" selector="[name='stop_rules_processing']"/> + </section> + + <section name="AdminCatalogPriceRuleGrid"> + <element name="applyRules" type="button" selector="#apply_rules" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml new file mode 100644 index 0000000000000..a68965d97e879 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml @@ -0,0 +1,240 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AdminCreateCatalogPriceRuleByPercentTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Create catalog price rule"/> + <title value="Admin should be able to create a catalog price rule applied as a percentage of original (for simple product)"/> + <description value="Admin should be able to create a catalog price rule applied as a percentage of original (for simple product)"/> + <severity value="MAJOR"/> + <testCaseId value="MC-65"/> + <group value="CatalogRule"/> + </annotations> + <before> + <!-- Create the simple product and category that it will be in --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- log in and create the price rule --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"/> + <click stepKey="saveAndApply" selector="{{AdminNewCatalogPriceRule.saveAndApply}}"/> + <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule."/> + </before> + <after> + <!-- delete the simple product and catalog price rule and logout --> + <amOnPage stepKey="goToPriceRulePage" url="admin/catalog_rule/promo_catalog/"/> + <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <argument name="name" value="{{_defaultCatalogRule.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to category page and make sure that all of the prices are correct --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategory"/> + <see stepKey="seeOldPrice" selector="{{StorefrontCategoryProductSection.ProductOldPriceByNumber('1')}}" userInput="$$createProduct.price$$"/> + <see stepKey="seeNewPrice" selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="$110.70"/> + + <!-- Go to the simple product page and check that the prices are correct --> + <amOnPage stepKey="goToProductPage" url="$$createProduct.sku$$.html"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <see stepKey="seeOldPriceTag" selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price"/> + <see stepKey="seeOldPrice2" selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" userInput="$$createProduct.price$$"/> + <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$110.70"/> + + <!-- Add the product to cart and check that the price is correct there --> + <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> + <waitForPageLoad stepKey="waitForAddedToCart"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForCart"/> + <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$110.70"/> + </test> + + <test name="AdminCreateCatalogPriceRuleByFixedTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Create catalog price rule"/> + <title value="Admin should be able to create a catalog price rule applied as a fixed amount (for simple product)"/> + <description value="Admin should be able to create a catalog price rule applied as a fixed amount (for simple product)"/> + <severity value="MAJOR"/> + <testCaseId value="MC-93"/> + <group value="CatalogRule"/> + </annotations> + <before> + <!-- Create the simple product and category that it will be in --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- log in and create the price rule --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"> + <argument name="catalogRule" value="CatalogRuleByFixed"/> + </actionGroup> + <click stepKey="saveAndApply" selector="{{AdminNewCatalogPriceRule.saveAndApply}}"/> + <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule."/> + </before> + <after> + <!-- delete the simple product and catalog price rule and logout --> + <amOnPage stepKey="goToPriceRulePage" url="admin/catalog_rule/promo_catalog/"/> + <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <argument name="name" value="{{CatalogRuleByFixed.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to category page and make sure that all of the prices are correct --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategory"/> + <see stepKey="seeOldPrice" selector="{{StorefrontCategoryProductSection.ProductOldPriceByNumber('1')}}" userInput="$$createProduct.price$$"/> + <see stepKey="seeNewPrice" selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="$113.00"/> + + <!-- Go to the simple product page and check that the prices are correct --> + <amOnPage stepKey="goToProductPage" url="$$createProduct.sku$$.html"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <see stepKey="seeOldPriceTag" selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price"/> + <see stepKey="seeOldPrice2" selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" userInput="$$createProduct.price$$"/> + <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$113.00"/> + + <!-- Add the product to cart and check that the price is correct there --> + <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> + <waitForPageLoad stepKey="waitForAddedToCart"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForCart"/> + <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$113.00"/> + </test> + + <test name="AdminCreateCatalogPriceRuleToPercentTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Create catalog price rule"/> + <title value="Admin should be able to create a catalog price rule adjust final price to this percentage (for simple product)"/> + <description value="Admin should be able to create a catalog price rule adjust final price to this percentage (for simple product)"/> + <severity value="MAJOR"/> + <testCaseId value="MC-69"/> + <group value="CatalogRule"/> + </annotations> + <before> + <!-- Create the simple product and category that it will be in --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- log in and create the price rule --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"> + <argument name="catalogRule" value="CatalogRuleToPercent"/> + </actionGroup> + <click stepKey="saveAndApply" selector="{{AdminNewCatalogPriceRule.saveAndApply}}"/> + <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule."/> + </before> + <after> + <!-- delete the simple product and catalog price rule and logout --> + <amOnPage stepKey="goToPriceRulePage" url="admin/catalog_rule/promo_catalog/"/> + <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <argument name="name" value="{{CatalogRuleToPercent.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to category page and make sure that all of the prices are correct --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategory"/> + <see stepKey="seeOldPrice" selector="{{StorefrontCategoryProductSection.ProductOldPriceByNumber('1')}}" userInput="$$createProduct.price$$"/> + <see stepKey="seeNewPrice" selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="$110.70"/> + + <!-- Go to the simple product page and check that the prices are correct --> + <amOnPage stepKey="goToProductPage" url="$$createProduct.sku$$.html"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <see stepKey="seeOldPriceTag" selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price"/> + <see stepKey="seeOldPrice2" selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" userInput="$$createProduct.price$$"/> + <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$110.70"/> + + <!-- Add the product to cart and check that the price is correct there --> + <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> + <waitForPageLoad stepKey="waitForAddedToCart"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForCart"/> + <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$110.70"/> + </test> + + <test name="AdminCreateCatalogPriceRuleToFixedTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Create catalog price rule"/> + <title value="Admin should be able to create a catalog price rule adjust final price to discount value (for simple product)"/> + <description value="Admin should be able to create a catalog price rule adjust final price to discount value (for simple product)"/> + <severity value="MAJOR"/> + <testCaseId value="MC-60"/> + <group value="CatalogRule"/> + </annotations> + <before> + <!-- Create the simple product and category that it will be in --> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- log in and create the price rule --> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + <actionGroup stepKey="createNewPriceRule" ref="newCatalogPriceRuleByUI"> + <argument name="catalogRule" value="CatalogRuleToFixed"/> + </actionGroup> + <click stepKey="saveAndApply" selector="{{AdminNewCatalogPriceRule.saveAndApply}}"/> + <see stepKey="assertSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule."/> + </before> + <after> + <!-- delete the simple product and catalog price rule and logout --> + <amOnPage stepKey="goToPriceRulePage" url="admin/catalog_rule/promo_catalog/"/> + <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <argument name="name" value="{{CatalogRuleToFixed.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Go to category page and make sure that all of the prices are correct --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategory"/> + <see stepKey="seeOldPrice" selector="{{StorefrontCategoryProductSection.ProductOldPriceByNumber('1')}}" userInput="$$createProduct.price$$"/> + <see stepKey="seeNewPrice" selector="{{StorefrontCategoryProductSection.ProductSpecialPriceByNumber('1')}}" userInput="$100.00"/> + + <!-- Go to the simple product page and check that the prices are correct --> + <amOnPage stepKey="goToProductPage" url="$$createProduct.sku$$.html"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <see stepKey="seeOldPriceTag" selector="{{StorefrontProductInfoMainSection.oldPriceTag}}" userInput="Regular Price"/> + <see stepKey="seeOldPrice2" selector="{{StorefrontProductInfoMainSection.oldPriceAmount}}" userInput="$$createProduct.price$$"/> + <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$100.00"/> + + <!-- Add the product to cart and check that the price is correct there --> + <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> + <waitForPageLoad stepKey="waitForAddedToCart"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForCart"/> + <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$100.00"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml new file mode 100644 index 0000000000000..77a9a48074c29 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -0,0 +1,60 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontInactiveCatalogRuleTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Customer view catalog price rule"/> + <title value="Customer should not see the catalog price rule promotion if status is inactive"/> + <description value="Customer should not see the catalog price rule promotion if status is inactive"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-79"/> + <group value="CatalogRule"/> + </annotations> + <before> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="newCatalogPriceRuleByUI" stepKey="createNewPriceRule"/> + <selectOption selector="{{AdminNewCatalogPriceRule.status}}" userInput="Inactive" stepKey="setInactive"/> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="seeSuccess"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="admin/catalog_rule/promo_catalog/" stepKey="goToPriceRulePage"/> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> + <argument name="name" value="{{_defaultCatalogRule.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + </after> + + <!-- Verify price is not discounted on category page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategory"/> + <waitForPageLoad stepKey="waitForCategory"/> + <see selector="{{StorefrontCategoryProductSection.ProductPriceByNumber('1')}}" userInput="$$createProduct.price$$" stepKey="seePrice1"/> + + <!-- Verify price is not discounted on the product page --> + <amOnPage url="$$createProduct.sku$$.html" stepKey="goToProduct"/> + <waitForPageLoad stepKey="waitForProduct"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createProduct.price$$" stepKey="seePrice2"/> + + <!-- Verify price is not discounted in the cart --> + <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> + <waitForPageLoad stepKey="waitForCart"/> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForCheckout"/> + <see selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$$createProduct.price$$" stepKey="seePrice3"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php index e266e67804e88..beff1c66d4f61 100644 --- a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php +++ b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php @@ -6,6 +6,7 @@ namespace Magento\CatalogSearch\Setup\Patch\Data; +use Magento\Framework\App\State; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; use Magento\Framework\Indexer\IndexerInterfaceFactory; @@ -27,17 +28,25 @@ class SetInitialSearchWeightForAttributes implements DataPatchInterface, PatchVe */ private $attributeRepository; + /** + * @var State + */ + private $state; + /** * SetInitialSearchWeightForAttributes constructor. * @param IndexerInterfaceFactory $indexerFactory * @param ProductAttributeRepositoryInterface $attributeRepository + * @param State $state */ public function __construct( IndexerInterfaceFactory $indexerFactory, - ProductAttributeRepositoryInterface $attributeRepository + ProductAttributeRepositoryInterface $attributeRepository, + State $state ) { $this->indexerFactory = $indexerFactory; $this->attributeRepository = $attributeRepository; + $this->state = $state; } /** @@ -47,6 +56,13 @@ public function apply() { $this->setWeight('sku', 6); $this->setWeight('name', 5); + $indexer = $this->indexerFactory->create()->load('catalogsearch_fulltext'); + $this->state->emulateAreaCode( + \Magento\Framework\App\Area::AREA_CRONTAB, + function () use ($indexer) { + $indexer->reindexAll(); + } + ); } /** @@ -76,8 +92,9 @@ public function getAliases() /** * Set attribute search weight. * - * @param $attributeCode - * @param $weight + * @param string $attributeCode + * @param int $weight + * @return void */ private function setWeight($attributeCode, $weight) { diff --git a/app/code/Magento/CatalogSearch/Setup/RecurringData.php b/app/code/Magento/CatalogSearch/Setup/RecurringData.php deleted file mode 100644 index 0c2aee800b6f1..0000000000000 --- a/app/code/Magento/CatalogSearch/Setup/RecurringData.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\CatalogSearch\Setup; - -use Magento\Framework\App\State; -use Magento\Framework\Indexer\IndexerInterfaceFactory; -use Magento\Framework\Setup\InstallDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; -use Magento\Framework\Setup\ModuleDataSetupInterface; - -/** - * Recurring data install. - */ -class RecurringData implements InstallDataInterface -{ - /** - * @var IndexerInterfaceFactory - */ - private $indexerInterfaceFactory; - /** - * @var State - */ - private $state; - - /** - * Init - * - * @param IndexerInterfaceFactory $indexerInterfaceFactory - */ - public function __construct( - IndexerInterfaceFactory $indexerInterfaceFactory, - State $state - ) { - $this->indexerInterfaceFactory = $indexerInterfaceFactory; - $this->state = $state; - } - - /** - * {@inheritdoc} - */ - public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) - { - $this->state->emulateAreaCode( - \Magento\Framework\App\Area::AREA_CRONTAB, - [$this, 'reindex'] - ); - } - - /** - * Run reindex. - * - * @return void - */ - public function reindex() - { - $this->indexerInterfaceFactory->create()->load('catalogsearch_fulltext')->reindexAll(); - } -} diff --git a/app/code/Magento/Checkout/Setup/Patch/Data/PrepareInitialCheckoutConfiguration.php b/app/code/Magento/Checkout/Setup/Patch/Data/PrepareInitialCheckoutConfiguration.php index bc38809d070b2..47a19fb3234fd 100644 --- a/app/code/Magento/Checkout/Setup/Patch/Data/PrepareInitialCheckoutConfiguration.php +++ b/app/code/Magento/Checkout/Setup/Patch/Data/PrepareInitialCheckoutConfiguration.php @@ -817,7 +817,7 @@ public function apply() $connection->commit(); } catch (\Exception $e) { - $connection->rollback(); + $connection->rollBack(); throw $e; } } diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml index 35ddfdaefe050..33b83fe63fdc1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml @@ -21,6 +21,24 @@ <waitForText userInput="{{productCount}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> </actionGroup> + <!-- Add Product to Cart from the category page with specified quantity and check message and product count in Minicart --> + <actionGroup name="StorefrontAddCategoryProductToCartWithQuantityActionGroup"> + <arguments> + <argument name="product"/> + <argument name="quantity" defaultValue="1" type="string"/> + <argument name="checkQuantity" defaultValue="1" type="string"/> + </arguments> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct" /> + <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart" /> + <!-- @TODO: Use general message selector after MQE-694 is fixed --> + <waitForElement selector="{{StorefrontMessagesSection.messageProductAddedToCart(product.name)}}" time="30" stepKey="assertMessage"/> + <waitForText userInput="{{checkQuantity}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/> + <conditionalClick selector="{{StorefrontMinicartSection.showCart}}" dependentSelector="{{StorefrontMinicartSection.miniCartOpened}}" visible="false" stepKey="openMiniCart"/> + <waitForElementVisible selector="{{StorefrontMinicartSection.viewAndEditCart}}" stepKey="waitForViewAndEditCartVisible"/> + <fillField selector="{{StorefrontMinicartSection.itemQuantity(product.name)}}" userInput="{{quantity}}" stepKey="setProductQtyToFiftyInMiniCart"/> + <click selector="{{StorefrontMinicartSection.itemQuantityUpdate(product.name)}}" stepKey="updateQtyInMiniCart"/> + </actionGroup> + <!-- Add Product to Cart from the product page and check message and product count in Minicart --> <actionGroup name="StorefrontAddProductToCartActionGroup"> <arguments> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/AdminDataGridHeaderSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/AdminDataGridHeaderSection.xml new file mode 100644 index 0000000000000..56062f96152c2 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/AdminDataGridHeaderSection.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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminDataGridHeaderSection"> + <element name="attributeCodeFilterInput" type="input" selector=".admin__data-grid-filters input[name='attribute_code']"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index c20309814d51d..136658cc59106 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -31,5 +31,6 @@ <element name="next" type="button" selector="button.button.action.continue.primary" timeout="30"/> <element name="firstShippingMethod" type="radio" selector="//*[@id='checkout-shipping-method-load']//input[@class='radio']"/> <element name="defaultShipping" type="button" selector=".billing-address-details"/> + <element name="stateInput" type="input" selector="input[name=region]"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml index 0fa5dc8a42341..0da16a34134b8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml @@ -23,5 +23,7 @@ <element name="viewAndEditCart" type="button" selector=".action.viewcart" timeout="30"/> <element name="miniCartItemsText" type="text" selector=".minicart-items"/> <element name="deleteMiniCartItem" type="button" selector=".action.delete" timeout="30"/> + <element name="itemQuantity" type="input" selector="//a[text()='{{productName}}']/../..//input[contains(@class,'cart-item-qty')]" parameterized="true"/> + <element name="itemQuantityUpdate" type="button" selector="//a[text()='{{productName}}']/../..//span[text()='Update']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml new file mode 100644 index 0000000000000..add1a1b1cf9be --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml @@ -0,0 +1,52 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="AddressStateFieldShouldNotAcceptJustIntegerValuesTest"> + <annotations> + <features value="Checkout"/> + <stories value="MAGETWO-91465"/> + <title value="Guest Checkout"/> + <description value="Address State field should not allow just integer values"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-93203"/> + <group value="checkout"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> + <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" /> + <selectOption stepKey="selectCounty" selector="{{CheckoutShippingSection.country}}" userInput="{{UK_Address.country_id}}"/> + <waitForPageLoad stepKey="waitFormToReload"/> + <fillField selector="{{CheckoutShippingSection.stateInput}}" userInput="1" stepKey="enterStateAsIntegerValue"/> + <waitForPageLoad stepKey="waitforFormValidation"/> + <see userInput="First character must be letter." stepKey="seeTheErrorMessageDisplayed"/> + <fillField selector="{{CheckoutShippingSection.stateInput}}" userInput=" 1" stepKey="enterStateAsIntegerValue1"/> + <waitForPageLoad stepKey="waitforFormValidation1"/> + <see userInput="First character must be letter." stepKey="seeTheErrorMessageDisplayed1"/> + <fillField selector="{{CheckoutShippingSection.stateInput}}" userInput="ABC1" stepKey="enterStateAsIntegerValue2"/> + <waitForPageLoad stepKey="waitforFormValidation2"/> + <dontSee userInput="First character must be letter." stepKey="seeTheErrorMessageIsNotDisplayed"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/CollectQuoteTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/CollectQuoteTest.php new file mode 100644 index 0000000000000..14410578b12e4 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/CollectQuoteTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model\Cart; + +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\RegionInterface; +use Magento\Checkout\Model\Cart\CollectQuote; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\EstimateAddressInterface; +use Magento\Quote\Api\Data\EstimateAddressInterfaceFactory; +use Magento\Quote\Api\ShippingMethodManagementInterface; +use Magento\Quote\Model\Quote; +use PHPUnit\Framework\TestCase; + +/** + * Class CollectQuoteTest + */ +class CollectQuoteTest extends TestCase +{ + /** + * @var CollectQuote + */ + private $model; + + /** + * @var CustomerSession|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerSessionMock; + + /** + * @var CustomerRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerRepositoryMock; + + /** + * @var AddressRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressRepositoryMock; + + /** + * @var EstimateAddressInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + */ + private $estimateAddressFactoryMock; + + /** + * @var EstimateAddressInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $estimateAddressMock; + + /** + * @var ShippingMethodManagementInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $shippingMethodManagerMock; + + /** + * @var CartRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteRepositoryMock; + + /** + * @var Quote|\PHPUnit_Framework_MockObject_MockObject + */ + private $quoteMock; + + /** + * @var CustomerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $customerMock; + + /** + * @var AddressInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $addressMock; + + /** + * Set up + */ + protected function setUp() + { + $this->customerSessionMock = $this->createMock(CustomerSession::class); + $this->customerRepositoryMock = $this->getMockForAbstractClass( + CustomerRepositoryInterface::class, + [], + '', + false, + true, + true, + ['getById'] + ); + $this->addressRepositoryMock = $this->createMock(AddressRepositoryInterface::class); + $this->estimateAddressMock = $this->createMock(EstimateAddressInterface::class); + $this->estimateAddressFactoryMock = + $this->createPartialMock(EstimateAddressInterfaceFactory::class, ['create']); + $this->shippingMethodManagerMock = $this->createMock(ShippingMethodManagementInterface::class); + $this->quoteRepositoryMock = $this->createMock(CartRepositoryInterface::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->customerMock = $this->createMock(CustomerInterface::class); + $this->addressMock = $this->createMock(AddressInterface::class); + + $this->model = new CollectQuote( + $this->customerSessionMock, + $this->customerRepositoryMock, + $this->addressRepositoryMock, + $this->estimateAddressFactoryMock, + $this->shippingMethodManagerMock, + $this->quoteRepositoryMock + ); + } + + /** + * Test collect method + */ + public function testCollect() + { + $customerId = 1; + $defaultAddressId = 999; + $countryId = 'USA'; + $regionId = 'CA'; + $regionMock = $this->createMock(RegionInterface::class); + + $this->customerSessionMock->expects(self::once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->customerSessionMock->expects(self::once()) + ->method('getCustomerId') + ->willReturn($customerId); + $this->customerRepositoryMock->expects(self::once()) + ->method('getById') + ->willReturn($this->customerMock); + $this->customerMock->expects(self::once()) + ->method('getDefaultShipping') + ->willReturn($defaultAddressId); + $this->addressMock->expects(self::once()) + ->method('getCountryId') + ->willReturn($countryId); + $regionMock->expects(self::once()) + ->method('getRegion') + ->willReturn($regionId); + $this->addressMock->expects(self::once()) + ->method('getRegion') + ->willReturn($regionMock); + $this->addressRepositoryMock->expects(self::once()) + ->method('getById') + ->with($defaultAddressId) + ->willReturn($this->addressMock); + $this->estimateAddressFactoryMock->expects(self::once()) + ->method('create') + ->willReturn($this->estimateAddressMock); + $this->quoteRepositoryMock->expects(self::once()) + ->method('save') + ->with($this->quoteMock); + + $this->model->collect($this->quoteMock); + } + + /** + * Test with a not logged in customer + */ + public function testCollectWhenCustomerIsNotLoggedIn() + { + $this->customerSessionMock->expects(self::once()) + ->method('isLoggedIn') + ->willReturn(false); + $this->customerRepositoryMock->expects(self::never()) + ->method('getById'); + + $this->model->collect($this->quoteMock); + } +} diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index 6947e1162600a..11e3ba5f3ed9a 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -41,6 +41,10 @@ <field id="number_items_to_display_pager" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Number of Items to Display Pager</label> </field> + <field id="crosssell_enabled" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <label>Show Cross-sell Items in the Shopping Cart</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="cart_link" translate="label" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="0"> <label>My Cart Link</label> diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index 3c24c38ecf85b..e1ba4381f2230 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -17,6 +17,7 @@ <delete_quote_after>30</delete_quote_after> <redirect_to_cart>0</redirect_to_cart> <number_items_to_display_pager>20</number_items_to_display_pager> + <crosssell_enabled>1</crosssell_enabled> </cart> <cart_link> <use_qty>1</use_qty> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index 53fdebb8a2995..a6ea2c13579a7 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -177,3 +177,8 @@ Payment,Payment "We received your order!","We received your order!" "Thank you for your purchase!","Thank you for your purchase!" "Password", "Password" +"Something went wrong while saving the page. Please refresh the page and try again.","Something went wrong while saving the page. Please refresh the page and try again." +"Item in Cart","Item in Cart" +"Items in Cart","Items in Cart" +"Close","Close" +"Show Cross-sell Items in the Shopping Cart","Show Cross-sell Items in the Shopping Cart" diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml index ff4c6dbd35ff2..69d2523d88dfb 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml @@ -186,7 +186,7 @@ </block> <container name="checkout.cart.widget" as="checkout_cart_widget" label="Shopping Cart Items After"/> </container> - <block class="Magento\Checkout\Block\Cart\Crosssell" name="checkout.cart.crosssell" template="Magento_Catalog::product/list/items.phtml" after="-"> + <block class="Magento\Checkout\Block\Cart\Crosssell" name="checkout.cart.crosssell" template="Magento_Catalog::product/list/items.phtml" after="-" ifconfig="checkout/cart/crosssell_enabled"> <arguments> <argument name="type" xsi:type="string">crosssell</argument> </arguments> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml new file mode 100644 index 0000000000000..2225d3d34a655 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CreateNewPageWithAllValuesActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateNewPageWithAllValues"> + <arguments> + <argument name="PageTitle" type="string"/> + <argument name="ContentHeading" type="string"/> + <argument name="URLKey" type="string"/> + <argument name="selectStoreViewOpt" type="string"/> + <argument name="selectHierarchyOpt" type="string"/> + </arguments> + <amOnPage url="{{CmsNewPagePage.url}}" stepKey="amOnCMSNewPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{PageTitle}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <fillField selector="{{CmsNewPagePageContentSection.contentHeading}}" userInput="{{ContentHeading}}" stepKey="fillFieldContentHeading"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimization"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{URLKey}}" stepKey="fillFieldURLKey"/> + <click selector="{{CmsNewPagePiwSection.header}}" stepKey="clickPageInWebsites"/> + <waitForElementVisible selector="{{CmsNewPagePiwSection.selectStoreView(selectStoreViewOpt)}}" stepKey="waitForStoreGridReload"/> + <clickWithLeftButton selector="{{CmsNewPagePiwSection.selectStoreView(selectStoreViewOpt)}}" stepKey="clickStoreView2"/> + <click selector="{{CmsNewPageHierarchySection.header}}" stepKey="clickHierarchy"/> + <click selector="{{CmsNewPageHierarchySection.selectHierarchy(selectHierarchyOpt)}}" stepKey="clickPageCheckBoxes"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml new file mode 100644 index 0000000000000..690ad9881c7fc --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/DeletePageByUrlKeyActionGroup.xml @@ -0,0 +1,23 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DeletePageByUrlKeyActionGroup"> + <arguments> + <argument name="UrlKey" type="string"/> + </arguments> + <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnCMSNewPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <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"/> + </actionGroup> +</actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageHierarchySection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageHierarchySection.xml new file mode 100644 index 0000000000000..e2c4f48f4ff9b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPageHierarchySection.xml @@ -0,0 +1,18 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CmsNewPageHierarchySection"> + <element name="header" type="button" selector="div[data-index=hierarchy]" timeout="30"/> + <element name="selectHierarchy" type="button" selector="//a/span[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> + </section> +</sections> + + + diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePiwSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePiwSection.xml new file mode 100644 index 0000000000000..456de55b49171 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewPagePiwSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="CmsNewPagePiwSection"> + <element name="header" type="button" selector="div[data-index=websites]" timeout="30"/> + <element name="selectStoreView" type="select" selector="//option[contains(text(),'{{var1}}')]" parameterized="true"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml index b27fb84e98a08..2f28aa46af65b 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml @@ -25,5 +25,7 @@ <element name="firstItemEditButton" type="button" selector=".data-grid .action-select-wrap .action-menu-item[data-action~='item-edit']"/> <element name="activeFilter" type="button" selector="(//div[contains(@class, 'admin__data-grid-filters-current') and contains(@class, '_show')])[1]"/> <element name="savePageSuccessMessage" type="text" selector=".message-success"/> + <element name="delete" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" parameterized="true"/> + <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml new file mode 100644 index 0000000000000..7bb2441a6a529 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ConfigSalesTaxClassActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SetTaxClassForShipping"> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{SalesConfigSection.TaxClassesTab}}" dependentSelector="{{SalesConfigSection.CheckIfTaxClassesTabExpand}}" visible="true" stepKey="expandTaxClassesTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ShippingTaxClass}}" stepKey="seeShippingTaxClass"/> + <uncheckOption selector="{{SalesConfigSection.EnableTaxClassForShipping}}" stepKey="uncheckUseSystemValue"/> + <selectOption selector="{{SalesConfigSection.ShippingTaxClass}}" userInput="Taxable Goods" stepKey="setShippingTaxClass"/> + <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> + </actionGroup> + <actionGroup name="ResetTaxClassForShipping"> + <amOnPage url="{{AdminSalesTaxClassPage.url}}" stepKey="navigateToSalesTaxConfigPagetoReset"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <conditionalClick selector="{{SalesConfigSection.TaxClassesTab}}" dependentSelector="{{SalesConfigSection.CheckIfTaxClassesTabExpand}}" visible="true" stepKey="openTaxClassTab"/> + <waitForElementVisible selector="{{SalesConfigSection.ShippingTaxClass}}" stepKey="seeShippingTaxClass2"/> + <selectOption selector="{{SalesConfigSection.ShippingTaxClass}}" userInput="None" stepKey="resetShippingTaxClass"/> + <checkOption selector="{{SalesConfigSection.EnableTaxClassForShipping}}" stepKey="useSystemValue"/> + <click selector="{{SalesConfigSection.TaxClassesTab}}" stepKey="collapseTaxClassesTab"/> + <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfiguration"/> + </actionGroup> + </actionGroups> \ No newline at end of file diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml index e517f6ef62c7a..c24e578c9109c 100644 --- a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml +++ b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml @@ -12,4 +12,7 @@ <page name="AdminContentManagementPage" url="admin/system_config/edit/section/cms/" area="admin" module="Magento_Config"> <section name="ContentManagementSection"/> </page> + <page name="AdminSalesTaxClassPage" url="admin/system_config/edit/section/tax/" area="admin" module="Magento_Config"> + <section name="SalesTaxClassSection"/> + </page> </pages> diff --git a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml new file mode 100644 index 0000000000000..f1520f5813e6d --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml @@ -0,0 +1,17 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="SalesConfigSection"> + <element name="TaxClassesTab" type="button" selector="#tax_classes-head"/> + <element name="CheckIfTaxClassesTabExpand" type="button" selector="#tax_classes-head:not(.open)"/> + <element name="ShippingTaxClass" type="select" selector="#tax_classes_shipping_tax_class"/> + <element name="EnableTaxClassForShipping" type="checkbox" selector="#tax_classes_shipping_tax_class_inherit"/> + </section> +</sections> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 29583231a764a..19de63b7a976c 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -926,6 +926,8 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p return $result; } } + } elseif (is_string($result)) { + return __($result)->render(); } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php index 3d42217de5f91..5581fcc07b861 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php @@ -64,7 +64,7 @@ public function resolvePrice(\Magento\Framework\Pricing\SaleableInterface $produ foreach ($this->lowestPriceOptionsProvider->getProducts($product) as $subProduct) { $productPrice = $this->priceResolver->resolvePrice($subProduct); - $price = $price ? min($price, $productPrice) : $productPrice; + $price = isset($price) ? min($price, $productPrice) : $productPrice; } return (float)$price; diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml index e04dbf274d932..77759043a0500 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminCreateProductConfigurationsPanelSection.xml @@ -18,6 +18,7 @@ <element name="id" type="text" selector="//tr[contains(@data-repeat-index, '0')]/td[2]/div"/> <element name="selectAll" type="button" selector=".action-select-all"/> + <element name="selectAllByAttribute" type="button" selector="//div[@data-attribute-title='{{attr}}']//button[contains(@class, 'action-select-all')]" parameterized="true"/> <element name="createNewValue" type="input" selector=".action-create-new" timeout="30"/> <element name="attributeName" type="input" selector="li[data-attribute-option-title=''] .admin__field-create-new .admin__control-text"/> <element name="saveAttribute" type="button" selector="li[data-attribute-option-title=''] .action-save" timeout="30"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml index f42a1f4d8dbd7..31787ca75f199 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -14,6 +14,7 @@ <element name="newAttributeIFrame" type="iframe" selector="create_new_attribute_container"/> <element name="defaultLabel" type="input" selector="input[name='frontend_label[0]']"/> <element name="inputType" type="select" selector="select[name='frontend_input']" timeout="30"/> + <element name="valuesRequired" type="select" selector="select#is_required"/> <element name="addOption" type="button" selector="#add_new_option_button"/> <element name="isDefault" type="radio" selector="[data-role='options-container'] tr:nth-of-type({{row}}) input[name='default[]']" parameterized="true"/> <element name="optionAdminValue" type="input" selector="[data-role='options-container'] input[name='option[value][option_{{row}}][0]']" parameterized="true"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml index 6b278237b6599..8a2cd192a20e3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml @@ -9,6 +9,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"> <section name="AdminProductFormConfigurationsSection"> + <element name="sectionHeader" type="text" selector=".admin__collapsible-block-wrapper[data-index='configurable']"/> <element name="createConfigurations" type="button" selector="button[data-index='create_configurable_products_button']" timeout="30"/> <element name="currentVariationsRows" type="button" selector=".data-row"/> <element name="currentVariationsNameCells" type="textarea" selector=".admin__control-fields[data-index='name_container']"/> @@ -31,11 +32,6 @@ <section name="AdminConfigurableProductSelectAttributesSlideOut"> <element name="grid" type="button" selector=".admin__data-grid-wrap tbody"/> </section> - <section name="AdminConfigurableProductAssignSourcesSlideOut"> - <element name="done" type="button" selector=".product_form_product_form_assign_sources_configurable_modal .action-primary" timeout="5"/> - <element name="assignSources" type="button" selector="(//button/span[contains(text(), 'Assign Sources')])[2]" timeout="5"/> - <element name="quantityPerSource" type="input" selector="input[name='quantity_resolver[dynamicRows][dynamicRows][0][quantity_per_source]']"/> - </section> <section name="StorefrontConfigurableProductPage"> <element name="productAttributeDropDown" type="select" selector="select[id*='attribute']"/> </section> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml index f1fae2218301f..c612431ec7044 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest.xml @@ -8,12 +8,13 @@ <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"> - <test name="AdminConfigurableProductOutOfStockTest"> + <test name="AdminConfigurableProductChildrenOutOfStockTest"> <annotations> <features value="ConfigurableProduct"/> <stories value="Product visibility when in stock/out of stock"/> <title value="Configurable Product goes 'Out of Stock' if all associated Simple Products are 'Out of Stock'"/> <description value="Configurable Product goes 'Out of Stock' if all associated Simple Products are 'Out of Stock'"/> + <severity value="CRITICAL"/> <testCaseId value="MC-181"/> <group value="ConfigurableProduct"/> </annotations> @@ -87,7 +88,7 @@ <!-- Check to make sure that the configurable product shows up as in stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage"/> <waitForPageLoad stepKey="waitForStoreFrontLoad"/> - <see stepKey="lookForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK" /> + <see stepKey="checkForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK" /> <!-- Find the first simple product that we just created using the product grid and go to its page--> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> @@ -103,12 +104,11 @@ <!-- Edit the quantity of the simple first product as 0 --> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> - <waitForPageLoad stepKey="waitForProductPageSaved"/> <!-- Check to make sure that the configurable product shows up as in stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> <waitForPageLoad stepKey="waitForStoreFrontLoad2"/> - <see stepKey="lookForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> + <see stepKey="checkForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> <!-- Find the second simple product that we just created using the product grid and go to its page--> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> @@ -124,11 +124,222 @@ <!-- Edit the quantity of the second simple product as 0 --> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> - <waitForPageLoad stepKey="waitForProductPageSaved2"/> <!-- Check to make sure that the configurable product shows up as out of stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage3"/> <waitForPageLoad stepKey="waitForStoreFrontLoad3"/> - <see stepKey="lookForOutOfStock3" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="OUT OF STOCK"/> + <see stepKey="checkForOutOfStock3" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="OUT OF STOCK"/> + </test> + + <test name="AdminConfigurableProductOutOfStockTestDeleteChildren"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product visibility when in stock/out of stock"/> + <title value="Configurable Product goes 'Out of Stock' if all associated Simple Products are deleted"/> + <description value="Configurable Product goes 'Out of Stock' if all associated Simple Products are deleted"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-3042"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category to put the product in --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- log in --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <see stepKey="checkForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK" /> + + <!-- Delete the first simple product --> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="{{ApiSimpleOne.sku}}"/> + </actionGroup> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad2"/> + <see stepKey="checkForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> + + <!-- Delete the second simple product --> + <actionGroup stepKey="deleteProduct2" ref="deleteProductBySku"> + <argument name="sku" value="{{ApiSimpleTwo.sku}}"/> + </actionGroup> + + <!-- Check to make sure that the configurable product shows up as out of stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage3"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad3"/> + <see stepKey="checkForOutOfStock3" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="OUT OF STOCK"/> + </test> + + <test name="AdminConfigurableProductOutOfStockAndDeleteCombinationTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Product visibility when in stock/out of stock"/> + <title value="Configurable Product goes 'Out of Stock' if all associated Simple Products are a combination of 'Out of Stock' and deleted"/> + <description value="Configurable Product goes 'Out of Stock' if all associated Simple Products are a combination of 'Out of Stock' and deleted"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-3046"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category to put the product in --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Create the configurable product based on the data in the /data folder --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Make the configurable product have two options, that are children of the default attribute set --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Create the 2 children that will be a part of the configurable product --> + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ApiSimpleTwo" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Assign the two products to the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- log in --> + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + </before> + + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + </after> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad"/> + <see stepKey="checkForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK" /> + + <!-- Delete the first simple product --> + <actionGroup stepKey="deleteProduct1" ref="deleteProductBySku"> + <argument name="sku" value="{{ApiSimpleOne.sku}}"/> + </actionGroup> + + <!-- Check to make sure that the configurable product shows up as in stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad2"/> + <see stepKey="checkForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> + + <!-- Find the second simple product that we just created using the product grid and go to its page--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <waitForPageLoad stepKey="waitForAdminProductGridLoad2"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> + <actionGroup ref="filterProductGridBySku" stepKey="findCreatedProduct2"> + <argument name="product" value="ApiSimpleTwo"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFiltersToBeApplied2"/> + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + + <!-- Edit the quantity of the second simple product as 0 --> + <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + + <!-- Check to make sure that the configurable product shows up as out of stock --> + <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage3"/> + <waitForPageLoad stepKey="waitForStoreFrontLoad3"/> + <see stepKey="checkForOutOfStock3" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="OUT OF STOCK"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml new file mode 100644 index 0000000000000..c0e7c886d0cc1 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -0,0 +1,161 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontConfigurableProductChildSearchTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="View configurable product details in storefront"/> + <title value="Guest customer should be able to search configurable product by attributes of child products"/> + <description value="Guest customer should be able to search configurable product by attributes of child products"/> + <severity value="MAJOR"/> + <testCaseId value="MC-249"/> + <group value="ConfigurableProduct"/> + </annotations> + <before> + <!-- TODO: This should be converted to an actionGroup once MQE-993 is fixed. --> + <!-- Create the category --> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <!-- Create the configurable product and add it to the category --> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create an attribute with two options to be used in the first child product --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption2" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Create an attribute with two options to be used in the second child product --> + <createData entity="productAttributeMultiselectTwoOptions" stepKey="createConfigProductAttributeMultiSelect"/> + <createData entity="productAttributeOption3" stepKey="createConfigProductAttributeOption1Multiselect"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + </createData> + <createData entity="productAttributeOption4" stepKey="createConfigProductAttributeOption2Multiselect"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + </createData> + + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Add the second attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet2"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + </createData> + + <!-- Get the first option of the attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Get the first option of the second attribute we created --> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + </getData> + + <!-- Create a simple product and give it the attribute with the first option --> + <createData entity="ApiSimpleOneHidden" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <updateData entity="ApiSimpleProductUpdateDescription" stepKey="updateSimpleProduct1" createDataKey="createConfigChildProduct1"/> + + <!-- Create a simple product and give it the attribute with the second option --> + <createData entity="ApiSimpleTwoHidden" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttributeMultiSelect"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + </createData> + + <!-- Add the first simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + + <!-- Add the second simple product to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + + <!-- Create an attribute with two options to be used in the first child product (in the UI) --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttributeSelect"/> + <createData entity="productAttributeOption5" stepKey="createConfigProductAttributeSelectOption1"> + <requiredEntity createDataKey="createConfigProductAttributeSelect"/> + </createData> + <createData entity="productAttributeOption6" stepKey="createConfigProductAttributeSelectOption2"> + <requiredEntity createDataKey="createConfigProductAttributeSelect"/> + </createData> + + <!-- Add the attribute we just created to default attribute set --> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet3"> + <requiredEntity createDataKey="createConfigProductAttributeSelect"/> + </createData> + + <actionGroup ref="LoginAsAdmin" stepKey="login"/> + + <!-- Go to the product page for the first product --> + <amOnPage stepKey="goToProductGrid" url="{{ProductCatalogPage.url}}"/> + <waitForPageLoad stepKey="waitForProductGridLoad"/> + <actionGroup stepKey="searchForSimpleProduct" ref="filterProductGridBySku2"> + <argument name="sku" value="$$createConfigChildProduct1.sku$$"/> + </actionGroup> + <actionGroup stepKey="openProductEditPage" ref="openProducForEditByClickingRowXColumnYInProductGrid"/> + + <!-- Edit the attribute for the first simple product --> + <selectOption stepKey="editSelectAttribute" selector="{{ModifyAttributes.nthExistingAttribute($$createConfigProductAttributeSelect.default_frontend_label$$)}}" userInput="$$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> + </before> + + <after> + <amOnPage url="admin/admin/auth/logout/" stepKey="logout"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> + <deleteData createDataKey="createConfigProductAttributeMultiSelect" stepKey="deleteConfigProductAttributeMultiSelect"/> + <deleteData createDataKey="createConfigProductAttributeSelect" stepKey="deleteConfigProductAttributeSelect"/> + <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> + </after> + + <!-- Quick search the storefront for the first attribute option --> + <amOnPage stepKey="goToStoreFront" url="{{StorefrontHomePage.url}}"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <fillField stepKey="searchStorefront1" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$"/> + <click stepKey="clickSearch1" selector="{{StorefrontQuickSearchSection.searchButton}}"/> + <seeElement stepKey="seeProduct1" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> + + <!-- Quick search the storefront for the second attribute option --> + <fillField stepKey="searchStorefront2" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigProductAttributeOption1Multiselect.option[store_labels][0][label]$$"/> + <click stepKey="clickSearch2" selector="{{StorefrontQuickSearchSection.searchButton}}"/> + <seeElement stepKey="seeProduct2" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> + + <!-- Quick search the storefront for the first product description --> + <fillField stepKey="searchStorefront3" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="'$$createConfigChildProduct1.custom_attributes[short_description]$$'"/> + <click stepKey="clickSearch3" selector="{{StorefrontQuickSearchSection.searchButton}}"/> + <seeElement stepKey="seeProduct3" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml new file mode 100644 index 0000000000000..e7091dbfae215 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontConfigurableProductWithFileCustomOptionTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Add configurable product to cart"/> + <title value="Correct error message and redirect with invalid file option"/> + <description value="Configurable product has file custom option. When adding to cart with an invalid filetype, the correct error message is shown, and options remain selected."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-93059"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create a configurable product via the UI --> + <actionGroup ref="createConfigurableProduct" stepKey="createProduct"> + <argument name="product" value="BaseConfigurableProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <!--Add custom option to configurable product--> + <actionGroup ref="AddProductCustomOptionFile" stepKey="addCustomOptionToProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + + <!--Go to storefront--> + <amOnPage url="" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForHomePageLoad"/> + <click selector="{{StorefrontNavigationSection.topCategory($$createCategory.name$$)}}" stepKey="goToCategoryStorefront"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="$$createCategory.name$$" stepKey="seeOnCategoryPage"/> + <!--Add configurable product to cart--> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductTitleByName(BaseConfigurableProduct.name)}}" stepKey="hoverProductInGrid"/> + <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(BaseConfigurableProduct.name)}}" stepKey="tryAddToCartFromCategoryPage"/> + <waitForPageLoad stepKey="waitForRedirectToProductPage"/> + <seeInCurrentUrl url="{{StorefrontProductPage.url(BaseConfigurableProduct.urlKey)}}" stepKey="seeOnProductPage"/> + <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="{{colorProductAttribute2.name}}" stepKey="selectColor"/> + <!--Try invalid file--> + <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" userInput="lorem_ipsum.docx" stepKey="attachInvalidFile"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCartInvalidFile"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.alertMessage}}" stepKey="waitForErrorMessageInvalidFile"/> + <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="The file 'lorem_ipsum.docx' for '{{ProductOptionFile.title}}' has an invalid extension." stepKey="seeMessageInvalidFile"/> + <!--Option remains selected--> + <seeOptionIsSelected selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeOptionRemainSelected"/> + <!--Try valid file--> + <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" userInput="{{MagentoLogo.file}}" stepKey="attachValidFile"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$11.99" stepKey="seePriceUpdated"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCartValidFile"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" stepKey="waitForSuccessMessage"/> + <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="You added {{BaseConfigurableProduct.name}} to your shopping cart." stepKey="seeSuccessMessage"/> + + <!--Check item in cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> + <waitForPageLoad stepKey="waitForCartPageLoad"/> + <seeElement selector="{{CheckoutCartProductSection.ProductLinkByName(BaseConfigurableProduct.name)}}" stepKey="seeProductInCart"/> + <see selector="{{CheckoutCartProductSection.ProductOptionByNameAndAttribute(BaseConfigurableProduct.name, colorProductAttribute.default_label)}}" userInput="{{colorProductAttribute2.name}}" stepKey="seeSelectedOption"/> + <see selector="{{CheckoutCartProductSection.ProductOptionByNameAndAttribute(BaseConfigurableProduct.name, ProductOptionFile.title)}}" userInput="{{MagentoLogo.file}}" stepKey="seeCorrectOptionFile"/> + <!--Delete cart item--> + <click selector="{{CheckoutCartProductSection.RemoveItem}}" stepKey="deleteCartItem"/> + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php index 99c31420473f5..189730e18080c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Pricing/Price/ConfigurablePriceResolverTest.php @@ -55,24 +55,31 @@ protected function setUp() * situation: one product is supplying the price, which could be a price of zero (0) * * @dataProvider resolvePriceDataProvider + * + * @param $variantPrices + * @param $expectedPrice */ - public function testResolvePrice($expectedValue) + public function testResolvePrice($variantPrices, $expectedPrice) { - $price = $expectedValue; - $product = $this->getMockBuilder( \Magento\Catalog\Model\Product::class )->disableOriginalConstructor()->getMock(); $product->expects($this->never())->method('getSku'); - $this->lowestPriceOptionsProvider->expects($this->once())->method('getProducts')->willReturn([$product]); - $this->priceResolver->expects($this->once()) + $products = array_map(function () { + return $this->getMockBuilder(\Magento\Catalog\Model\Product::class) + ->disableOriginalConstructor() + ->getMock(); + }, $variantPrices); + + $this->lowestPriceOptionsProvider->expects($this->once())->method('getProducts')->willReturn($products); + $this->priceResolver ->method('resolvePrice') - ->with($product) - ->willReturn($price); + ->willReturnOnConsecutiveCalls(...$variantPrices); - $this->assertEquals($expectedValue, $this->resolver->resolvePrice($product)); + $actualPrice = $this->resolver->resolvePrice($product); + self::assertSame($expectedPrice, $actualPrice); } /** @@ -81,8 +88,40 @@ public function testResolvePrice($expectedValue) public function resolvePriceDataProvider() { return [ - 'price of zero' => [0.00], - 'price of five' => [5], + 'Single variant at price 0.00 (float), should return 0.00 (float)' => [ + $variantPrices = [ + 0.00, + ], + $expectedPrice = 0.00, + ], + 'Single variant at price 5 (integer), should return 5.00 (float)' => [ + $variantPrices = [ + 5, + ], + $expectedPrice = 5.00, + ], + 'Single variants at price null (null), should return 0.00 (float)' => [ + $variantPrices = [ + null, + ], + $expectedPrice = 0.00, + ], + 'Multiple variants at price 0, 10, 20, should return 0.00 (float)' => [ + $variantPrices = [ + 0, + 10, + 20, + ], + $expectedPrice = 0.00, + ], + 'Multiple variants at price 10, 0, 20, should return 0.00 (float)' => [ + $variantPrices = [ + 10, + 0, + 20, + ], + $expectedPrice = 0.00, + ], ]; } } diff --git a/app/code/Magento/ConfigurableProduct/etc/adminhtml/system.xml b/app/code/Magento/ConfigurableProduct/etc/adminhtml/system.xml index ba52b51d6b077..86baea3c0d296 100644 --- a/app/code/Magento/ConfigurableProduct/etc/adminhtml/system.xml +++ b/app/code/Magento/ConfigurableProduct/etc/adminhtml/system.xml @@ -9,7 +9,7 @@ <system> <section id="checkout"> <group id="cart"> - <field id="configurable_product_image" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="configurable_product_image" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Configurable Product Image</label> <source_model>Magento\Catalog\Model\Config\Source\Product\Thumbnail</source_model> </field> diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml index 9b8e2c0c8c0bd..f5ed067967547 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/frontend/templates/product/view/type/options/configurable.phtml @@ -38,6 +38,9 @@ $_attributes = $block->decorateArray($block->getAllowAttributes()); "gallerySwitchStrategy": "<?php /* @escapeNotVerified */ echo $block->getVar('gallery_switch_strategy', 'Magento_ConfigurableProduct') ?: 'replace'; ?>" } + }, + "*" : { + "Magento_ConfigurableProduct/js/catalog-add-to-cart": {} } } </script> diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/catalog-add-to-cart.js new file mode 100644 index 0000000000000..3e6a611c268af --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/catalog-add-to-cart.js @@ -0,0 +1,20 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +require([ + 'jquery' +], function ($) { + 'use strict'; + + /** + * Add selected configurable attributes to redirect url + * + * @see Magento_Catalog/js/catalog-add-to-cart + */ + $('body').on('catalogCategoryAddToCartRedirect', function (event, data) { + $(data.form).find('select[name*="super"]').each(function (index, item) { + data.redirectParameters.push(item.config.id + '=' + $(item).val()); + }); + }); +}); diff --git a/app/code/Magento/Customer/Model/Address/Validator/Country.php b/app/code/Magento/Customer/Model/Address/Validator/Country.php index ff1020eba70ef..d7fb51dbbd442 100644 --- a/app/code/Magento/Customer/Model/Address/Validator/Country.php +++ b/app/code/Magento/Customer/Model/Address/Validator/Country.php @@ -88,6 +88,8 @@ private function validateCountry(AbstractAddress $address) * * @param AbstractAddress $address * @return array + * @throws \Zend_Validate_Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function validateRegion(AbstractAddress $address) { @@ -107,7 +109,7 @@ private function validateRegion(AbstractAddress $address) //If country actually has regions and requires you to //select one then it must be selected. $errors[] = __('"%fieldName" is required. Enter and try again.', ['fieldName' => 'regionId']); - } elseif ($regionId && !in_array($regionId, $allowedRegions, true)) { + } elseif ($allowedRegions && $regionId && !in_array($regionId, $allowedRegions, true)) { //If a region is selected then checking if it exists. $errors[] = __( 'Invalid value of "%value" provided for the %fieldName field.', diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml index 531e0f4b23b91..de8418774f794 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SignUpNewUserFromStorefrontActionGroup.xml @@ -12,6 +12,7 @@ <argument name="Customer"/> </arguments> <amOnPage stepKey="amOnStorefrontPage" url="/"/> + <waitForPageLoad stepKey="waitForStorefrontPage"/> <click stepKey="clickOnCreateAccountLink" selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}"/> <fillField stepKey="fillFirstName" userInput="{{Customer.firstname}}" selector="{{StorefrontCustomerCreateFormSection.firstnameField}}"/> <fillField stepKey="fillLastName" userInput="{{Customer.lastname}}" selector="{{StorefrontCustomerCreateFormSection.lastnameField}}"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index 19194ae2e5423..a1f0277ec40ec 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -83,4 +83,8 @@ <data key="default_shipping">Yes</data> <requiredEntity type="region">RegionCA</requiredEntity> </entity> + <!--If required other field can be added to UK_Address entity, dont modify any existing data--> + <entity name="UK_Address" type="address"> + <data key="country_id">GB</data> + </entity> </entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml index 0d443172e0c66..21205c6d5d91e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerDashboardAccountInformationSection.xml @@ -22,6 +22,6 @@ <element name="stateProvince" type="select" selector="#region_id"/> <element name="zip" type="input" selector="#zip"/> <element name="country" type="select" selector="#country"/> - <element name="saveAddress" type="button" selector="[data-action='save-address']"/> + <element name="saveAddress" type="button" selector="[data-action='save-address']" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml index d1a3e44416349..65e7aa7a12113 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontPanelHeaderSection.xml @@ -10,6 +10,6 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> <section name="StorefrontPanelHeaderSection"> <element name="WelcomeMessage" type="text" selector=".greet.welcome span"/> - <element name="createAnAccountLink" type="select" selector=".panel.header li:nth-child(3)"/> + <element name="createAnAccountLink" type="select" selector=".panel.header li:nth-child(3)" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index adca5b7ec4d2d..bde15b31ff1e6 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -25,6 +25,7 @@ <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <waitForPageLoad stepKey="waitForLoad1"/> <click selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}" stepKey="clickCreateCustomer"/> <waitForElement selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="wait1"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php index 9ab35f2d301d4..d8148543a55db 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Address/Validator/CountryTest.php @@ -161,7 +161,13 @@ public function validateDataProvider() 'region_id2' => [ array_merge($data, ['country_id' => $countryId, 'region_id' => 2]), [$countryId++], - [1], + [], + [], + ], + 'region_id3' => [ + array_merge($data, ['country_id' => $countryId, 'region_id' => 2]), + [$countryId++], + [1, 3], ['Invalid value of "2" provided for the regionId field.'], ], 'validated' => [ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php index f3070c46ebb7b..50c21379054bf 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/DataProviderTest.php @@ -1269,7 +1269,7 @@ public function testGetDataWithVisibleAttributesWithAccountEdit() $helper = new ObjectManager($this); $context = $this->getMockBuilder(\Magento\Framework\View\Element\UiComponent\ContextInterface::class) ->setMethods(['getRequestParam']) - ->getMockforAbstractClass(); + ->getMockForAbstractClass(); $context->expects($this->any()) ->method('getRequestParam') ->with('request-field-name') diff --git a/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml b/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml index 10223df489e90..24657a6846cae 100644 --- a/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml @@ -13,7 +13,7 @@ if ($block->isLoggedIn()) { $dataPostParam = sprintf(" data-post='%s'", $block->getPostParams()); } ?> -<li class="authorization-link" data-label="<?= $block->escapeHtmlAttr(__('or')) ?>"> +<li class="authorization-link" data-label="<?= $block->escapeHtml(__('or')) ?>"> <a <?= /* @noEscape */ $block->getLinkAttributes() ?><?= /* @noEscape */ $dataPostParam ?>> <?= $block->escapeHtml($block->getLabel()) ?> </a> diff --git a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml index 1f1f078504524..6a129a3aa4b44 100644 --- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml @@ -112,7 +112,7 @@ name="region" value="<?= $block->escapeHtmlAttr($block->getRegion()) ?>" title="<?= $block->escapeHtmlAttr(__('State/Province')) ?>" - class="input-text <?= $block->escapeHtmlAttr($this->helper('Magento\Customer\Helper\Address')->getAttributeValidationClass('region')) ?>"<?= !$block->getConfig('general/region/display_all') ? ' disabled="disabled"' : '' ?>/> + class="input-text validate-not-number-first <?= $block->escapeHtmlAttr($this->helper('Magento\Customer\Helper\Address')->getAttributeValidationClass('region')) ?>"<?= !$block->getConfig('general/region/display_all') ? ' disabled="disabled"' : '' ?>/> </div> </div> <div class="field zip required"> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/confirmation.phtml b/app/code/Magento/Customer/view/frontend/templates/form/confirmation.phtml index 0de3d94334970..8edb21ac8e1e9 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/confirmation.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/confirmation.phtml @@ -14,7 +14,7 @@ <div class="field email required"> <label for="email_address" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> - <input type="email" name="email" id="email_address" class="input-text" value="<?= $block->escapeHtmlAttr($block->getEmail()) ?>" data-validate="{required:true, 'validate-email':true}"> + <input type="email" name="email" id="email_address" class="input-text" value="<?= $block->escapeHtmlAttr($block->getEmail()) ?>" data-validate="{required:true, 'validate-email':true}" data-mage-init='{"mage/trim-input":{}}'> </div> </div> </fieldset> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml index 2d44dde215139..d85e76e4cbc3f 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml @@ -19,7 +19,7 @@ id="login-form" data-mage-init='{"validation":{}}'> <?= $block->getBlockHtml('formkey') ?> - <fieldset class="fieldset login" data-hasrequired="<?= $block->escapeHtmlAttr(__('* Required Fields')) ?>"> + <fieldset class="fieldset login" data-hasrequired="<?= $block->escapeHtml(__('* Required Fields')) ?>"> <div class="field note"><?= $block->escapeHtml(__('If you have an account, sign in with your email address.')) ?></div> <div class="field email required"> <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index e5cc543db6aac..ab940c9e84533 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -593,6 +593,18 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) if (in_array($attributeCode, $this->_ignoredAttributes)) { continue; } + + $isFieldRequired = $attributeParams['is_required']; + $isFieldNotSetAndCustomerDoesNotExist = + !isset($rowData[$attributeCode]) && !$this->_getCustomerId($email, $website); + $isFieldSetAndTrimmedValueIsEmpty + = isset($rowData[$attributeCode]) && '' === trim($rowData[$attributeCode]); + + if ($isFieldRequired && ($isFieldNotSetAndCustomerDoesNotExist || $isFieldSetAndTrimmedValueIsEmpty)) { + $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); + continue; + } + if (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) { $this->isAttributeValid( $attributeCode, @@ -603,8 +615,6 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) ? $this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR] : Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR ); - } elseif ($attributeParams['is_required'] && !$this->_getCustomerId($email, $website)) { - $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); } } } diff --git a/app/code/Magento/Developer/Model/Di/PluginList.php b/app/code/Magento/Developer/Model/Di/PluginList.php index 0a1df8a974242..fc342b5051000 100644 --- a/app/code/Magento/Developer/Model/Di/PluginList.php +++ b/app/code/Magento/Developer/Model/Di/PluginList.php @@ -145,7 +145,10 @@ private function addPluginToList($pluginInstance, $pluginMethod, $methodTypes, $ if (!array_key_exists($pluginInstance, $this->pluginList[$this->pluginTypeMapping[$typeCode]])) { $this->pluginList[$this->pluginTypeMapping[$typeCode]][$pluginInstance] = []; } - $this->pluginList[$this->pluginTypeMapping[$typeCode]][$pluginInstance][] = $pluginMethod ; + + if (!in_array($pluginMethod, $this->pluginList[$this->pluginTypeMapping[$typeCode]][$pluginInstance])) { + $this->pluginList[$this->pluginTypeMapping[$typeCode]][$pluginInstance][] = $pluginMethod; + } } } } diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 258dcbe9595d4..6262d71c4edcc 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -1263,8 +1263,20 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) * * @param \Magento\Framework\DataObject $request * @return $this|\Magento\Framework\DataObject|boolean + * @deprecated */ public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + { + return $this->processAdditionalValidation($request); + } + + /** + * Processing additional validation to check is carrier applicable. + * + * @param \Magento\Framework\DataObject $request + * @return $this|\Magento\Framework\DataObject|boolean + */ + public function processAdditionalValidation(\Magento\Framework\DataObject $request) { //Skip by item validation if there is no items in request if (!count($this->getAllItems($request))) { diff --git a/app/code/Magento/Directory/etc/zip_codes.xml b/app/code/Magento/Directory/etc/zip_codes.xml index d9041d1ff50a7..3c540f7ce0ffd 100644 --- a/app/code/Magento/Directory/etc/zip_codes.xml +++ b/app/code/Magento/Directory/etc/zip_codes.xml @@ -196,7 +196,7 @@ </zip> <zip countryCode="IL"> <codes> - <code id="pattern_1" active="true" example="12345">^[0-9]{5}$</code> + <code id="pattern_1" active="true" example="6687865">^[0-9]{7}$</code> </codes> </zip> <zip countryCode="IT"> diff --git a/app/code/Magento/Downloadable/Block/Catalog/Product/Samples.php b/app/code/Magento/Downloadable/Block/Catalog/Product/Samples.php index 120c7a38a8f41..859d6a53d0618 100644 --- a/app/code/Magento/Downloadable/Block/Catalog/Product/Samples.php +++ b/app/code/Magento/Downloadable/Block/Catalog/Product/Samples.php @@ -6,7 +6,8 @@ namespace Magento\Downloadable\Block\Catalog\Product; -use Magento\Downloadable\Model\ResourceModel\Sample; +use Magento\Downloadable\Model\ResourceModel\Sample\Collection as SampleCollection; +use Magento\Downloadable\Api\Data\SampleInterface; /** * Downloadable Product Samples part block @@ -29,7 +30,7 @@ public function hasSamples() /** * Get downloadable product samples * - * @return array + * @return SampleCollection */ public function getSamples() { @@ -37,7 +38,7 @@ public function getSamples() } /** - * @param Sample $sample + * @param SampleInterface $sample * @return string */ public function getSampleUrl($sample) diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php index 8ac79a0aa0d49..7dd6b0a19ec02 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php @@ -18,7 +18,7 @@ class Js extends \Magento\Backend\Block\Template * @var string */ - protected $_template = 'attribute/edit/js.phtml'; + protected $_template = 'Magento_Eav::attribute/edit/js.phtml'; /** * @var \Magento\Eav\Model\Adminhtml\System\Config\Source\Inputtype diff --git a/app/code/Magento/Eav/Test/Unit/Model/ResourceModel/Attribute/CollectionTest.php b/app/code/Magento/Eav/Test/Unit/Model/ResourceModel/Attribute/CollectionTest.php index 544cd9893e49e..f074fa22778b0 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/ResourceModel/Attribute/CollectionTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/ResourceModel/Attribute/CollectionTest.php @@ -115,7 +115,7 @@ protected function setUp() $this->connectionMock->expects($this->any())->method('quoteIdentifier')->will($this->returnArgument(0)); $this->connectionMock->expects($this->any()) ->method('describeTable') - ->will($this->returnvalueMap( + ->will($this->returnValueMap( [ [ 'some_main_table', diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 37ca2d8d7b378..c6651cdde0cb3 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -4,6 +4,10 @@ type Query { } +type Mutation { + placeholderMutation: String @doc(description: "Mutation type cannot be declared without fields. The placeholder will be removed when at least one mutation field is declared.") +} + input FilterTypeInput @doc(description: "FilterTypeInput specifies which action will be performed in a query ") { eq: String @doc(description: "Equals") finset: [String] @doc(description: "Find in set. The value can contain a set of comma-separated values") @@ -30,4 +34,4 @@ type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navig enum SortEnum @doc(description: "This enumeration indicates whether to return results in ascending or descending order") { ASC DESC -} \ No newline at end of file +} diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminGroupedProductActionGroup.xml b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminGroupedProductActionGroup.xml index bf870d3d9e2dd..720f5a2c2b949 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminGroupedProductActionGroup.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminGroupedProductActionGroup.xml @@ -43,4 +43,13 @@ <see selector="{{AdminProductGridSection.firstProductRow}}" userInput="{{product.name}}" stepKey="seeProductNameInGrid"/> <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> </actionGroup> + + <!--Fill product min quantity in group products grid--> + <actionGroup name="fillDefaultQuantityForLinkedToGroupProductInGrid"> + <arguments> + <argument name="productName" type="string"/> + <argument name="qty" type="string"/> + </arguments> + <fillField selector="{{AdminAddedProductsToGroupGrid.inputByProductName(productName)}}" userInput="{{qty}}" stepKey="fillDefaultQtyForLinkedProduct"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminAddProductsToGroupPanelSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminAddProductsToGroupPanelSection.xml index a03b1b34c1a5b..d21a067d34e20 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminAddProductsToGroupPanelSection.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminAddProductsToGroupPanelSection.xml @@ -16,4 +16,8 @@ <element name="firstCheckbox" type="input" selector="tr[data-repeat-index='0'] .admin__control-checkbox"/> <element name="nThCheckbox" type="input" selector="tr[data-repeat-index='{{n}}'] .admin__control-checkbox" parameterized="true"/> </section> + + <section name="AdminAddedProductsToGroupGrid"> + <element name="inputByProductName" type="input" selector="//div[@data-index='grouped']//table//tr[td[@data-index='name']//span[text()='{{productName}}']]//td[@data-index='qty']//input" parameterized="true"/> + </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/GroupedProduct/etc/adminhtml/system.xml b/app/code/Magento/GroupedProduct/etc/adminhtml/system.xml index bbb2054abb14f..5854928afc2fd 100644 --- a/app/code/Magento/GroupedProduct/etc/adminhtml/system.xml +++ b/app/code/Magento/GroupedProduct/etc/adminhtml/system.xml @@ -9,7 +9,7 @@ <system> <section id="checkout" translate="label" type="text" sortOrder="305" showInDefault="1" showInWebsite="1" showInStore="1"> <group id="cart" translate="label" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1"> - <field id="grouped_product_image" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> + <field id="grouped_product_image" translate="label" type="select" sortOrder="4" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Grouped Product Image</label> <source_model>Magento\Catalog\Model\Config\Source\Product\Thumbnail</source_model> </field> diff --git a/app/code/Magento/ImportExport/Files/Sample/customer.csv b/app/code/Magento/ImportExport/Files/Sample/customer.csv index 64c09574aea73..522e59f566739 100644 --- a/app/code/Magento/ImportExport/Files/Sample/customer.csv +++ b/app/code/Magento/ImportExport/Files/Sample/customer.csv @@ -1,2 +1,2 @@ -email,_website,_store,confirmation,created_at,created_in,disable_auto_group_change,dob,firstname,gender,group_id,lastname,middlename,password_hash,prefix,reward_update_notification,reward_warning_notification,rp_token,rp_token_created_at,store_id,suffix,taxvat,updated_at,website_id,password -jondoe@example.com,base,default,,"2015-10-30 12:49:47","Default Store View",0,,Jon,,1,Doe,,d708be3fe0fe0120840e8b13c8faae97424252c6374227ff59c05814f1aecd79:mgLqkqgTwLPLlCljzvF8hp67fNOOvOZb:1,,,,07e71459c137f4da15292134ff459cba,"2015-10-30 12:49:48",1,,,"2015-10-30 12:49:48",1, +email,_website,_store,confirmation,created_at,created_in,disable_auto_group_change,dob,firstname,gender,group_id,lastname,middlename,password_hash,prefix,rp_token,rp_token_created_at,store_id,suffix,taxvat,updated_at,website_id,password +jondoe@example.com,base,default,,"2015-10-30 12:49:47","Default Store View",0,,Jon,,1,Doe,,d708be3fe0fe0120840e8b13c8faae97424252c6374227ff59c05814f1aecd79:mgLqkqgTwLPLlCljzvF8hp67fNOOvOZb:1,,07e71459c137f4da15292134ff459cba,"2015-10-30 12:49:48",1,,,"2015-10-30 12:49:48",1, diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php index 079b2e60c4785..7f49e2022c410 100644 --- a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php @@ -77,7 +77,7 @@ public function addError( $errorMessage = null, $errorDescription = null ) { - if ($this->isErrorAlreadyAdded($rowNumber, $errorCode)) { + if ($this->isErrorAlreadyAdded($rowNumber, $errorCode, $columnName)) { return $this; } $this->processErrorStatistics($errorLevel); @@ -333,13 +333,14 @@ public function clear() /** * @param int $rowNum * @param string $errorCode + * @param string $columnName * @return bool */ - protected function isErrorAlreadyAdded($rowNum, $errorCode) + protected function isErrorAlreadyAdded($rowNum, $errorCode, $columnName = null) { $errors = $this->getErrorsByCode([$errorCode]); foreach ($errors as $error) { - if ($rowNum == $error->getRowNumber()) { + if ($rowNum == $error->getRowNumber() && $columnName == $error->getColumnName()) { return true; } } diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml index 093830d6fdb1c..fbdd394783608 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml @@ -64,6 +64,7 @@ require([ this.modifyFilterGrid(); } } else { + this.previousGridEntity = ''; $('export_filter_container').hide(); } } diff --git a/app/code/Magento/MediaStorage/Service/ImageResize.php b/app/code/Magento/MediaStorage/Service/ImageResize.php index 37dd6f485f1a8..6e3929296e252 100644 --- a/app/code/Magento/MediaStorage/Service/ImageResize.php +++ b/app/code/Magento/MediaStorage/Service/ImageResize.php @@ -202,7 +202,7 @@ private function getViewImages(array $themes): array ]); $images = $config->getMediaEntities('Magento_Catalog', ImageHelper::MEDIA_TYPE_CONFIG_NODE); foreach ($images as $imageId => $imageData) { - $uniqIndex = $this->getUniqImageIndex($imageData); + $uniqIndex = $this->getUniqueImageIndex($imageData); $imageData['id'] = $imageId; $viewImages[$uniqIndex] = $imageData; } @@ -211,11 +211,11 @@ private function getViewImages(array $themes): array } /** - * Get uniq image index + * Get unique image index * @param array $imageData * @return string */ - private function getUniqImageIndex(array $imageData): string + private function getUniqueImageIndex(array $imageData): string { ksort($imageData); unset($imageData['type']); diff --git a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php index c4620862b2e10..f301fb9289efb 100644 --- a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php +++ b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php @@ -139,6 +139,8 @@ private function canBeRun($consumerName, array $allowedConsumers = []) */ private function getPidFilePath($consumerName) { - return $consumerName . static::PID_FILE_EXT; + $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); + + return $consumerName . '-' . $sanitizedHostname . static::PID_FILE_EXT; } } diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php index 08c8f926522de..bf8796a03f52a 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php @@ -51,6 +51,8 @@ class ConsumersRunnerTest extends \PHPUnit\Framework\TestCase */ protected function setUp() { + require_once __DIR__ . '/../../_files/consumers_runner_functions_mocks.php'; + $this->phpExecutableFinderMock = $this->getMockBuilder(phpExecutableFinder::class) ->disableOriginalConstructor() ->getMock(); @@ -116,7 +118,7 @@ public function testRun( $isRunExpects ) { $consumerName = 'consumerName'; - $pidFilePath = 'consumerName.pid'; + $pidFilePath = 'consumerName-myHostName.pid'; $this->deploymentConfigMock->expects($this->exactly(3)) ->method('get') @@ -164,7 +166,7 @@ public function runDataProvider() 'isRun' => false, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName.pid', '--max-messages=20000'], + 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=20000'], 'allowedConsumers' => [], 'shellBackgroundExpects' => 1, 'isRunExpects' => 1, @@ -174,7 +176,7 @@ public function runDataProvider() 'isRun' => false, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], 'allowedConsumers' => [], 'shellBackgroundExpects' => 1, 'isRunExpects' => 1, @@ -184,7 +186,7 @@ public function runDataProvider() 'isRun' => false, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], 'allowedConsumers' => ['someConsumer'], 'shellBackgroundExpects' => 0, 'isRunExpects' => 0, @@ -194,7 +196,7 @@ public function runDataProvider() 'isRun' => true, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], 'allowedConsumers' => ['someConsumer'], 'shellBackgroundExpects' => 0, 'isRunExpects' => 0, @@ -204,7 +206,7 @@ public function runDataProvider() 'isRun' => true, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], 'allowedConsumers' => [], 'shellBackgroundExpects' => 0, 'isRunExpects' => 1, @@ -214,7 +216,7 @@ public function runDataProvider() 'isRun' => true, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], 'allowedConsumers' => ['consumerName'], 'shellBackgroundExpects' => 0, 'isRunExpects' => 1, @@ -224,7 +226,7 @@ public function runDataProvider() 'isRun' => false, 'php' => '', 'command' => 'php '. BP . '/bin/magento queue:consumers:start %s %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName.pid', '--max-messages=10000'], + 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid', '--max-messages=10000'], 'allowedConsumers' => ['consumerName'], 'shellBackgroundExpects' => 1, 'isRunExpects' => 1, @@ -234,7 +236,7 @@ public function runDataProvider() 'isRun' => false, 'php' => '/bin/php', 'command' => '/bin/php '. BP . '/bin/magento queue:consumers:start %s %s', - 'arguments' => ['consumerName', '--pid-file-path=consumerName.pid'], + 'arguments' => ['consumerName', '--pid-file-path=consumerName-myHostName.pid'], 'allowedConsumers' => ['consumerName'], 'shellBackgroundExpects' => 1, 'isRunExpects' => 1, diff --git a/app/code/Magento/MessageQueue/Test/Unit/_files/consumers_runner_functions_mocks.php b/app/code/Magento/MessageQueue/Test/Unit/_files/consumers_runner_functions_mocks.php new file mode 100644 index 0000000000000..788a8464a0ce4 --- /dev/null +++ b/app/code/Magento/MessageQueue/Test/Unit/_files/consumers_runner_functions_mocks.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model\Cron; + +/** + * @return string + */ +function gethostname() +{ + return 'myHost@Name'; +} diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Problem.php b/app/code/Magento/Newsletter/Block/Adminhtml/Problem.php index c5f4dc68d4dd2..61a17d7ad5e51 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Problem.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Problem.php @@ -19,7 +19,7 @@ class Problem extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'problem/list.phtml'; + protected $_template = 'Magento_Newsletter::problem/list.phtml'; /** * @var \Magento\Newsletter\Model\ResourceModel\Problem\Collection diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php index f085b0f6c9c8b..ca90b5d84a10f 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Edit.php @@ -19,7 +19,7 @@ class Edit extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'queue/edit.phtml'; + protected $_template = 'Magento_Newsletter::queue/edit.phtml'; /** * Core registry diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Subscriber.php b/app/code/Magento/Newsletter/Block/Adminhtml/Subscriber.php index 86e7e7ee4756d..4d5165db68736 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Subscriber.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Subscriber.php @@ -29,7 +29,7 @@ class Subscriber extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'subscriber/list.phtml'; + protected $_template = 'Magento_Newsletter::subscriber/list.phtml'; /** * @var \Magento\Newsletter\Model\ResourceModel\Queue\CollectionFactory diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Template.php b/app/code/Magento/Newsletter/Block/Adminhtml/Template.php index 02b60e049f254..92ae6e6c3db04 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Template.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Template.php @@ -16,7 +16,7 @@ class Template extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'template/list.phtml'; + protected $_template = 'Magento_Newsletter::template/list.phtml'; /** * @return $this diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Problem/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Problem/Collection.php index c0c5dfef277b8..0d7ebd74f724f 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Problem/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Problem/Collection.php @@ -165,8 +165,8 @@ protected function _addCustomersData() $customerName = $this->_customerView->getCustomerName($customer); foreach ($problems as $problem) { $problem->setCustomerName($customerName) - ->setCustomerFirstName($customer->getFirstName()) - ->setCustomerLastName($customer->getLastName()); + ->setCustomerFirstName($customer->getFirstname()) + ->setCustomerLastName($customer->getLastname()); } } catch (NoSuchEntityException $e) { // do nothing if customer is not found by id diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php index 9e98998f7f6ec..189549d2a73d2 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php @@ -221,7 +221,7 @@ public function addOnlyForSendingFilter() [\Magento\Newsletter\Model\Queue::STATUS_SENDING, \Magento\Newsletter\Model\Queue::STATUS_NEVER] )->where( 'main_table.queue_start_at < ?', - $this->_date->gmtdate() + $this->_date->gmtDate() )->where( 'main_table.queue_start_at IS NOT NULL' ); diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml index b2e0edbf04452..c1a526ee95148 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml @@ -22,9 +22,9 @@ <label class="label" for="newsletter"><span><?= $block->escapeHtml(__('Sign Up for Our Newsletter:')) ?></span></label> <div class="control"> <input name="email" type="email" id="newsletter" - placeholder="<?= $block->escapeHtmlAttr(__('Enter your email address')) ?>" - data-mage-init='{"mage/trim-input":{}}' - data-validate="{required:true, 'validate-email':true}"/> + placeholder="<?= $block->escapeHtml(__('Enter your email address')) ?>" + data-mage-init='{"mage/trim-input":{}}' + data-validate="{required:true, 'validate-email':true}"/> </div> </div> <div class="actions"> diff --git a/app/code/Magento/OfflinePayments/Block/Form/Banktransfer.php b/app/code/Magento/OfflinePayments/Block/Form/Banktransfer.php index c4fe10c386645..d60348d9dc1c7 100644 --- a/app/code/Magento/OfflinePayments/Block/Form/Banktransfer.php +++ b/app/code/Magento/OfflinePayments/Block/Form/Banktransfer.php @@ -15,5 +15,5 @@ class Banktransfer extends \Magento\OfflinePayments\Block\Form\AbstractInstructi * * @var string */ - protected $_template = 'form/banktransfer.phtml'; + protected $_template = 'Magento_OfflinePayments::form/banktransfer.phtml'; } diff --git a/app/code/Magento/OfflinePayments/Block/Form/Cashondelivery.php b/app/code/Magento/OfflinePayments/Block/Form/Cashondelivery.php index 4e0f7d48ce09b..de0f7a57bae62 100644 --- a/app/code/Magento/OfflinePayments/Block/Form/Cashondelivery.php +++ b/app/code/Magento/OfflinePayments/Block/Form/Cashondelivery.php @@ -15,5 +15,5 @@ class Cashondelivery extends \Magento\OfflinePayments\Block\Form\AbstractInstruc * * @var string */ - protected $_template = 'form/cashondelivery.phtml'; + protected $_template = 'Magento_OfflinePayments::form/cashondelivery.phtml'; } diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php index fc36eaf6a97db..0c4718245928b 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate.php @@ -320,7 +320,7 @@ public function getConditionName(\Magento\Framework\DataObject $object) */ private function getCsvFile($filePath) { - $pathInfo = pathInfo($filePath); + $pathInfo = pathinfo($filePath); $dirName = isset($pathInfo['dirname']) ? $pathInfo['dirname'] : ''; $fileName = isset($pathInfo['basename']) ? $pathInfo['basename'] : ''; diff --git a/app/code/Magento/Payment/Block/Info/Instructions.php b/app/code/Magento/Payment/Block/Info/Instructions.php index e3c74e020f8f6..687c6b54a2f4f 100644 --- a/app/code/Magento/Payment/Block/Info/Instructions.php +++ b/app/code/Magento/Payment/Block/Info/Instructions.php @@ -23,7 +23,7 @@ class Instructions extends \Magento\Payment\Block\Info /** * @var string */ - protected $_template = 'info/instructions.phtml'; + protected $_template = 'Magento_Payment::info/instructions.phtml'; /** * Get instructions text from order payment diff --git a/app/code/Magento/Paypal/Block/Adminhtml/Billing/Agreement/View/Form.php b/app/code/Magento/Paypal/Block/Adminhtml/Billing/Agreement/View/Form.php index 31b3e0c1d6b6f..396c66d4e748e 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/Billing/Agreement/View/Form.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/Billing/Agreement/View/Form.php @@ -13,5 +13,5 @@ class Form extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'billing/agreement/view/form.phtml'; + protected $_template = 'Magento_Paypal::billing/agreement/view/form.phtml'; } diff --git a/app/code/Magento/Paypal/Block/Adminhtml/Billing/Agreement/View/Tab/Info.php b/app/code/Magento/Paypal/Block/Adminhtml/Billing/Agreement/View/Tab/Info.php index d133d19f9c202..39373017fa09a 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/Billing/Agreement/View/Tab/Info.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/Billing/Agreement/View/Tab/Info.php @@ -15,7 +15,7 @@ class Info extends \Magento\Backend\Block\Template implements \Magento\Backend\B /** * @var string */ - protected $_template = 'billing/agreement/view/tab/info.phtml'; + protected $_template = 'Magento_Paypal::billing/agreement/view/tab/info.phtml'; /** * Core registry diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Payflowlink/Advanced.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Payflowlink/Advanced.php index fe4f6ee1f6757..793fad152bc7e 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Payflowlink/Advanced.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Payflowlink/Advanced.php @@ -16,5 +16,5 @@ class Advanced extends \Magento\Paypal\Block\Adminhtml\System\Config\Payflowlink * * @var string */ - protected $_template = 'system/config/payflowlink/advanced.phtml'; + protected $_template = 'Magento_Paypal::system/config/payflowlink/advanced.phtml'; } diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Payflowlink/Info.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Payflowlink/Info.php index 30119063f5b5b..405de1ec03185 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Payflowlink/Info.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Payflowlink/Info.php @@ -16,7 +16,7 @@ class Info extends \Magento\Config\Block\System\Config\Form\Field * * @var string */ - protected $_template = 'system/config/payflowlink/info.phtml'; + protected $_template = 'Magento_Paypal::system/config/payflowlink/info.phtml'; /** * Render fieldset html diff --git a/app/code/Magento/Paypal/Block/Hosted/Pro/Form.php b/app/code/Magento/Paypal/Block/Hosted/Pro/Form.php index 92281d3058eef..70eff8f83ba99 100644 --- a/app/code/Magento/Paypal/Block/Hosted/Pro/Form.php +++ b/app/code/Magento/Paypal/Block/Hosted/Pro/Form.php @@ -15,5 +15,5 @@ class Form extends \Magento\Payment\Block\Form /** * @var string */ - protected $_template = 'hss/info.phtml'; + protected $_template = 'Magento_Paypal::hss/info.phtml'; } diff --git a/app/code/Magento/Paypal/Block/Iframe.php b/app/code/Magento/Paypal/Block/Iframe.php index d6edb1ed25231..98fc05d0d2f60 100644 --- a/app/code/Magento/Paypal/Block/Iframe.php +++ b/app/code/Magento/Paypal/Block/Iframe.php @@ -44,7 +44,7 @@ class Iframe extends \Magento\Payment\Block\Form /** * @var string */ - protected $_template = 'hss/js.phtml'; + protected $_template = 'Magento_Paypal::hss/js.phtml'; /** * @var \Magento\Sales\Model\OrderFactory diff --git a/app/code/Magento/Paypal/Block/Payflow/Advanced/Form.php b/app/code/Magento/Paypal/Block/Payflow/Advanced/Form.php index d777279ad47a8..159abd4e2f1bb 100644 --- a/app/code/Magento/Paypal/Block/Payflow/Advanced/Form.php +++ b/app/code/Magento/Paypal/Block/Payflow/Advanced/Form.php @@ -15,7 +15,7 @@ class Form extends \Magento\Paypal\Block\Payflow\Link\Form /** * @var string */ - protected $_template = 'payflowadvanced/info.phtml'; + protected $_template = 'Magento_Paypal::payflowadvanced/info.phtml'; /** * Get frame action URL diff --git a/app/code/Magento/Paypal/Block/Payflow/Link/Form.php b/app/code/Magento/Paypal/Block/Payflow/Link/Form.php index f414a63d69046..fc880f494c859 100644 --- a/app/code/Magento/Paypal/Block/Payflow/Link/Form.php +++ b/app/code/Magento/Paypal/Block/Payflow/Link/Form.php @@ -15,7 +15,7 @@ class Form extends \Magento\Payment\Block\Form /** * @var string */ - protected $_template = 'payflowlink/info.phtml'; + protected $_template = 'Magento_Paypal::payflowlink/info.phtml'; /** * Get frame action URL diff --git a/app/code/Magento/ProductAlert/Block/Email/Price.php b/app/code/Magento/ProductAlert/Block/Email/Price.php index 982b0f7f63375..0430a21dc8bfd 100644 --- a/app/code/Magento/ProductAlert/Block/Email/Price.php +++ b/app/code/Magento/ProductAlert/Block/Email/Price.php @@ -15,7 +15,7 @@ class Price extends \Magento\ProductAlert\Block\Email\AbstractEmail /** * @var string */ - protected $_template = 'email/price.phtml'; + protected $_template = 'Magento_ProductAlert::email/price.phtml'; /** * Retrieve unsubscribe url for product diff --git a/app/code/Magento/ProductAlert/Block/Email/Stock.php b/app/code/Magento/ProductAlert/Block/Email/Stock.php index f424e7d7125c4..d01960b8eb855 100644 --- a/app/code/Magento/ProductAlert/Block/Email/Stock.php +++ b/app/code/Magento/ProductAlert/Block/Email/Stock.php @@ -15,7 +15,7 @@ class Stock extends \Magento\ProductAlert\Block\Email\AbstractEmail /** * @var string */ - protected $_template = 'email/stock.phtml'; + protected $_template = 'Magento_ProductAlert::email/stock.phtml'; /** * Retrieve unsubscribe url for product diff --git a/app/code/Magento/Quote/Model/CouponManagement.php b/app/code/Magento/Quote/Model/CouponManagement.php index 62515a17f268b..55c21c974d6dd 100644 --- a/app/code/Magento/Quote/Model/CouponManagement.php +++ b/app/code/Magento/Quote/Model/CouponManagement.php @@ -55,6 +55,9 @@ public function set($cartId, $couponCode) if (!$quote->getItemsCount()) { throw new NoSuchEntityException(__('The "%1" Cart doesn\'t contain products.', $cartId)); } + if (!$quote->getStoreId()) { + throw new NoSuchEntityException(__('Cart isn\'t assigned to correct store')); + } $quote->getShippingAddress()->setCollectShippingRates(true); try { diff --git a/app/code/Magento/Quote/Test/Unit/Model/CouponManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/CouponManagementTest.php index e6ba50e35b4c3..91211904c11eb 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/CouponManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/CouponManagementTest.php @@ -47,6 +47,7 @@ protected function setUp() 'save', 'getShippingAddress', 'getCouponCode', + 'getStoreId', '__wakeup' ]); $this->quoteAddressMock = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [ @@ -98,6 +99,9 @@ public function testSetWhenCouldNotApplyCoupon() $cartId = 33; $couponCode = '153a-ABC'; + $this->storeMock->expects($this->any())->method('getId')->will($this->returnValue(1)); + $this->quoteMock->expects($this->once())->method('getStoreId')->willReturn($this->returnValue(1)); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive')->with($cartId)->will($this->returnValue($this->quoteMock)); $this->quoteMock->expects($this->once())->method('getItemsCount')->will($this->returnValue(12)); @@ -125,6 +129,9 @@ public function testSetWhenCouponCodeIsInvalid() $cartId = 33; $couponCode = '153a-ABC'; + $this->storeMock->expects($this->any())->method('getId')->will($this->returnValue(1)); + $this->quoteMock->expects($this->once())->method('getStoreId')->willReturn($this->returnValue(1)); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive')->with($cartId)->will($this->returnValue($this->quoteMock)); $this->quoteMock->expects($this->once())->method('getItemsCount')->will($this->returnValue(12)); @@ -144,6 +151,9 @@ public function testSet() $cartId = 33; $couponCode = '153a-ABC'; + $this->storeMock->expects($this->any())->method('getId')->will($this->returnValue(1)); + $this->quoteMock->expects($this->once())->method('getStoreId')->willReturn($this->returnValue(1)); + $this->quoteRepositoryMock->expects($this->once()) ->method('getActive')->with($cartId)->will($this->returnValue($this->quoteMock)); $this->quoteMock->expects($this->once())->method('getItemsCount')->will($this->returnValue(12)); diff --git a/app/code/Magento/Quote/Test/Unit/Model/GuestCart/GuestCartManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/GuestCart/GuestCartManagementTest.php index ee0ffd3bcc666..73ed2e65b41a9 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/GuestCart/GuestCartManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/GuestCart/GuestCartManagementTest.php @@ -94,7 +94,7 @@ public function testCreateEmptyCart() $cartId = 1; $this->quoteIdMaskMock->expects($this->once())->method('setQuoteId')->with($cartId)->willReturnSelf(); $this->quoteIdMaskMock->expects($this->once())->method('save')->willReturnSelf(); - $this->quoteIdMaskMock->expects($this->once())->method('getMaskedId')->willreturn($maskedCartId); + $this->quoteIdMaskMock->expects($this->once())->method('getMaskedId')->willReturn($maskedCartId); $this->quoteIdMaskFactoryMock->expects($this->once())->method('create')->willReturn($this->quoteIdMaskMock); $this->quoteManagementMock->expects($this->once())->method('createEmptyCart')->willReturn($cartId); diff --git a/app/code/Magento/Reports/Block/Adminhtml/Product/Viewed.php b/app/code/Magento/Reports/Block/Adminhtml/Product/Viewed.php index fc4cffbdca408..f901b32d8b12f 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Product/Viewed.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Product/Viewed.php @@ -17,7 +17,7 @@ class Viewed extends \Magento\Backend\Block\Widget\Grid\Container /** * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * @return void diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Bestsellers.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Bestsellers.php index d70930d2395ae..b773184408a7f 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Bestsellers.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Bestsellers.php @@ -19,7 +19,7 @@ class Bestsellers extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Coupons.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Coupons.php index b8f71158877bb..fe85af58b34f6 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Coupons.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Coupons.php @@ -19,7 +19,7 @@ class Coupons extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Invoiced.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Invoiced.php index c96483e33ebe5..57594a11bd997 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Invoiced.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Invoiced.php @@ -19,7 +19,7 @@ class Invoiced extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Refunded.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Refunded.php index 7ff80f62f6bee..994b29e6eb0dd 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Refunded.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Refunded.php @@ -19,7 +19,7 @@ class Refunded extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales.php index 5abea45e657d7..64375ace3e94d 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Sales.php @@ -19,7 +19,7 @@ class Sales extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Shipping.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Shipping.php index 44dd4521c7bbe..e4dbdc2737745 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Shipping.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Shipping.php @@ -19,7 +19,7 @@ class Shipping extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax.php b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax.php index 38de08314d257..fa9e63745a87d 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Sales/Tax.php @@ -19,7 +19,7 @@ class Tax extends \Magento\Backend\Block\Widget\Grid\Container * * @var string */ - protected $_template = 'report/grid/container.phtml'; + protected $_template = 'Magento_Reports::report/grid/container.phtml'; /** * {@inheritdoc} diff --git a/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php b/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php index 28f2011de3365..1ca76cb1cf95f 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php @@ -18,7 +18,7 @@ class Wishlist extends \Magento\Backend\Block\Template * * @var string */ - protected $_template = 'report/wishlist.phtml'; + protected $_template = 'Magento_Reports::report/wishlist.phtml'; /** * Reports wishlist collection factory diff --git a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php index 0841388252905..dbf0a79bc42ff 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rating/Edit/Tab/Form.php @@ -17,7 +17,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic /** * @var string */ - protected $_template = 'rating/form.phtml'; + protected $_template = 'Magento_Review::rating/form.phtml'; /** * Session diff --git a/app/code/Magento/Review/Block/Adminhtml/Rss/Grid/Link.php b/app/code/Magento/Review/Block/Adminhtml/Rss/Grid/Link.php index 5d2ec9fc186ca..def0e896fc95f 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Rss/Grid/Link.php +++ b/app/code/Magento/Review/Block/Adminhtml/Rss/Grid/Link.php @@ -16,7 +16,7 @@ class Link extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'rss/grid/link.phtml'; + protected $_template = 'Magento_Review::rss/grid/link.phtml'; /** * @var \Magento\Framework\App\Rss\UrlBuilderInterface diff --git a/app/code/Magento/Review/Block/Customer/Recent.php b/app/code/Magento/Review/Block/Customer/Recent.php index 8f593f5695812..5c7f1ec2c0dad 100644 --- a/app/code/Magento/Review/Block/Customer/Recent.php +++ b/app/code/Magento/Review/Block/Customer/Recent.php @@ -20,7 +20,7 @@ class Recent extends \Magento\Framework\View\Element\Template * * @var string */ - protected $_template = 'customer/list.phtml'; + protected $_template = 'Magento_Review::customer/list.phtml'; /** * Product reviews collection diff --git a/app/code/Magento/Review/Block/Customer/View.php b/app/code/Magento/Review/Block/Customer/View.php index b7dfd4b969a9d..237b972f16573 100644 --- a/app/code/Magento/Review/Block/Customer/View.php +++ b/app/code/Magento/Review/Block/Customer/View.php @@ -23,7 +23,7 @@ class View extends \Magento\Catalog\Block\Product\AbstractProduct * * @var string */ - protected $_template = 'customer/view.phtml'; + protected $_template = 'Magento_Review::customer/view.phtml'; /** * Catalog product model diff --git a/app/code/Magento/Review/Block/Rating/Entity/Detailed.php b/app/code/Magento/Review/Block/Rating/Entity/Detailed.php index de871d9061428..0ce4f436ae704 100644 --- a/app/code/Magento/Review/Block/Rating/Entity/Detailed.php +++ b/app/code/Magento/Review/Block/Rating/Entity/Detailed.php @@ -15,7 +15,7 @@ class Detailed extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'detailed.phtml'; + protected $_template = 'Magento_Review::detailed.phtml'; /** * @var \Magento\Review\Model\RatingFactory diff --git a/app/code/Magento/Review/Block/View.php b/app/code/Magento/Review/Block/View.php index e2d0355671688..95b7176b48c44 100644 --- a/app/code/Magento/Review/Block/View.php +++ b/app/code/Magento/Review/Block/View.php @@ -19,7 +19,7 @@ class View extends \Magento\Catalog\Block\Product\AbstractProduct * * @var string */ - protected $_template = 'view.phtml'; + protected $_template = 'Magento_Review::view.phtml'; /** * Rating option model diff --git a/app/code/Magento/Rss/Block/Feeds.php b/app/code/Magento/Rss/Block/Feeds.php index 2e88d25c02891..86998f87f5c17 100644 --- a/app/code/Magento/Rss/Block/Feeds.php +++ b/app/code/Magento/Rss/Block/Feeds.php @@ -16,7 +16,7 @@ class Feeds extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'feeds.phtml'; + protected $_template = 'Magento_Rss::feeds.phtml'; /** * @var \Magento\Framework\App\Rss\RssManagerInterface diff --git a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php index 1ed9d59d29a72..87c15e474d11f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php @@ -5,6 +5,8 @@ */ namespace Magento\Sales\Block\Adminhtml\Items\Column; +use Magento\Framework\Filter\TruncateFilter\Result; + /** * Sales Order items name column renderer * @@ -13,6 +15,11 @@ */ class Name extends \Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn { + /** + * @var Result + */ + private $truncateResult = null; + /** * Truncate string * @@ -22,13 +29,15 @@ class Name extends \Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn * @param string &$remainder * @param bool $breakWords * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function truncateString($value, $length = 80, $etc = '...', &$remainder = '', $breakWords = true) { - return $this->filterManager->truncate( + $this->truncateResult = $this->filterManager->truncateFilter( $value, - ['length' => $length, 'etc' => $etc, 'remainder' => $remainder, 'breakWords' => $breakWords] + ['length' => $length, 'etc' => $etc, 'breakWords' => $breakWords] ); + return $this->truncateResult->getValue(); } /** @@ -40,8 +49,11 @@ public function truncateString($value, $length = 80, $etc = '...', &$remainder = public function getFormattedOption($value) { $remainder = ''; - $value = $this->truncateString($value, 55, '', $remainder); - $result = ['value' => nl2br($value), 'remainder' => nl2br($remainder)]; + $this->truncateString($value, 55, '', $remainder); + $result = [ + 'value' => nl2br($this->truncateResult->getValue()), + 'remainder' => nl2br($this->truncateResult->getRemainder()) + ]; return $result; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php index f2b454260dc22..6cab109b44dbb 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Address/Form.php @@ -19,7 +19,7 @@ class Form extends \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address * * @var string */ - protected $_template = 'order/address/form.phtml'; + protected $_template = 'Magento_Sales::order/address/form.phtml'; /** * Core registry diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Grandtotal.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Grandtotal.php index eb437915ad668..cf9f8a44dee27 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Grandtotal.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Grandtotal.php @@ -20,7 +20,7 @@ class Grandtotal extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Defa * * @var string */ - protected $_template = 'order/create/totals/grandtotal.phtml'; + protected $_template = 'Magento_Sales::order/create/totals/grandtotal.phtml'; /** * Tax config diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Shipping.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Shipping.php index 9225d8c2e5f68..34a9ed8070e26 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Shipping.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Shipping.php @@ -20,7 +20,7 @@ class Shipping extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Defaul * * @var string */ - protected $_template = 'order/create/totals/shipping.phtml'; + protected $_template = 'Magento_Sales::order/create/totals/shipping.phtml'; /** * Tax config diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Subtotal.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Subtotal.php index cfdd73de9d8b8..166f3c9637ebb 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Subtotal.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Subtotal.php @@ -20,7 +20,7 @@ class Subtotal extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Defaul * * @var string */ - protected $_template = 'order/create/totals/subtotal.phtml'; + protected $_template = 'Magento_Sales::order/create/totals/subtotal.phtml'; /** * Tax config diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php index d3da37c3f1bf8..207a4eca60213 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php @@ -18,5 +18,5 @@ class Tax extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\DefaultTota * * @var string */ - protected $_template = 'order/create/totals/tax.phtml'; + protected $_template = 'Magento_Sales::order/create/totals/tax.phtml'; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php index 5c3a7fce805cc..261f4b0cfd12a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php @@ -14,5 +14,5 @@ class Details extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/details.phtml'; + protected $_template = 'Magento_Sales::order/details.phtml'; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Form.php index 82c3effcab62d..6c06e9d624c81 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Form.php @@ -17,5 +17,5 @@ class Form extends \Magento\Backend\Block\Template * * @var string */ - protected $_template = 'order/view/form.phtml'; + protected $_template = 'Magento_Sales::order/view/form.phtml'; } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php index 5489a0b2e513f..64b53d10d4af6 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Tab/History.php @@ -18,7 +18,7 @@ class History extends \Magento\Backend\Block\Template implements \Magento\Backen * * @var string */ - protected $_template = 'order/view/tab/history.phtml'; + protected $_template = 'Magento_Sales::order/view/tab/history.phtml'; /** * Core registry diff --git a/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php b/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php index fbb78970a4de0..512539824da20 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Rss/Order/Grid/Link.php @@ -14,7 +14,7 @@ class Link extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'rss/order/grid/link.phtml'; + protected $_template = 'Magento_Sales::rss/order/grid/link.phtml'; /** * @var \Magento\Framework\App\Rss\UrlBuilderInterface diff --git a/app/code/Magento/Sales/Block/Order/Info/Buttons.php b/app/code/Magento/Sales/Block/Order/Info/Buttons.php index a27b55cd8543f..18e79f6a76ecf 100644 --- a/app/code/Magento/Sales/Block/Order/Info/Buttons.php +++ b/app/code/Magento/Sales/Block/Order/Info/Buttons.php @@ -20,7 +20,7 @@ class Buttons extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/info/buttons.phtml'; + protected $_template = 'Magento_Sales::order/info/buttons.phtml'; /** * Core registry diff --git a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php index 77e20eaa8d07b..2b84b8f1444b6 100644 --- a/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php +++ b/app/code/Magento/Sales/Block/Order/Info/Buttons/Rss.php @@ -16,7 +16,7 @@ class Rss extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/info/buttons/rss.phtml'; + protected $_template = 'Magento_Sales::order/info/buttons/rss.phtml'; /** * @var \Magento\Sales\Model\OrderFactory diff --git a/app/code/Magento/Sales/Block/Order/Invoice.php b/app/code/Magento/Sales/Block/Order/Invoice.php index 2d8448ea5bc98..24ddf4bac7696 100644 --- a/app/code/Magento/Sales/Block/Order/Invoice.php +++ b/app/code/Magento/Sales/Block/Order/Invoice.php @@ -18,7 +18,7 @@ class Invoice extends \Magento\Sales\Block\Order\Invoice\Items /** * @var string */ - protected $_template = 'order/invoice.phtml'; + protected $_template = 'Magento_Sales::order/invoice.phtml'; /** * @var \Magento\Framework\App\Http\Context diff --git a/app/code/Magento/Sales/Block/Order/View.php b/app/code/Magento/Sales/Block/Order/View.php index 870e2e15ab7b3..03d1340e0f690 100644 --- a/app/code/Magento/Sales/Block/Order/View.php +++ b/app/code/Magento/Sales/Block/Order/View.php @@ -18,7 +18,7 @@ class View extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/view.phtml'; + protected $_template = 'Magento_Sales::order/view.phtml'; /** * Core registry diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Unhold.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Unhold.php index 752ab088689c8..fa9676856a442 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Unhold.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Unhold.php @@ -32,7 +32,7 @@ public function execute() if (!$order->canUnhold()) { throw new \Magento\Framework\Exception\LocalizedException(__('Can\'t unhold order.')); } - $this->orderManagement->unhold($order->getEntityId()); + $this->orderManagement->unHold($order->getEntityId()); $this->messageManager->addSuccess(__('You released the order from holding status.')); } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addError($e->getMessage()); diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php index bb173f4010311..0cc4143e569db 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Total/Discount.php @@ -25,7 +25,7 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) * Calculate how much shipping discount should be applied * basing on how much shipping should be refunded. */ - $baseShippingAmount = (float)$creditmemo->getBaseShippingAmount(); + $baseShippingAmount = $this->getBaseShippingAmount($creditmemo); if ($baseShippingAmount) { $baseShippingDiscount = $baseShippingAmount * $order->getBaseShippingDiscountAmount() / @@ -75,4 +75,21 @@ public function collect(\Magento\Sales\Model\Order\Creditmemo $creditmemo) $creditmemo->setBaseGrandTotal($creditmemo->getBaseGrandTotal() - $baseTotalDiscountAmount); return $this; } + + /** + * Get base shipping amount + * + * @param \Magento\Sales\Model\Order\Creditmemo $creditmemo + * @return float + */ + private function getBaseShippingAmount(\Magento\Sales\Model\Order\Creditmemo $creditmemo): float + { + $baseShippingAmount = (float)$creditmemo->getBaseShippingAmount(); + if (!$baseShippingAmount) { + $baseShippingInclTax = (float)$creditmemo->getBaseShippingInclTax(); + $baseShippingTaxAmount = (float)$creditmemo->getBaseShippingTaxAmount(); + $baseShippingAmount = $baseShippingInclTax - $baseShippingTaxAmount; + } + return $baseShippingAmount; + } } 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 92d00d0436634..f06da0de0fd00 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/OrderSender.php @@ -138,7 +138,7 @@ protected function prepareTemplate(Order $order) */ $this->eventManager->dispatch( 'email_order_set_template_vars_before', - ['sender' => $this, 'transport' => $transportObject->getData(), 'transportObject' => $transportObject] + ['sender' => $this, 'transport' => $transportObject, 'transportObject' => $transportObject] ); $this->templateContainer->setTemplateVars($transportObject->getData()); diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml index 25dfdce165aaa..7ce10d5e5424e 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrderActionGroup.xml @@ -22,6 +22,24 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> </actionGroup> + <!--Navigate to create order page (New Order -> Select Customer)--> + <actionGroup name="navigateToNewOrderPageExistingCustomer"> + <arguments> + <argument name="customer"/> + </arguments> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> + <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> + <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> + <waitForPageLoad stepKey="waitForCustomerGridLoad"/> + <fillField userInput="{{customer.email}}" selector="{{AdminOrderCustomersGridSection.emailInput}}" stepKey="filterEmail"/> + <click selector="{{AdminOrderCustomersGridSection.apply}}" stepKey="applyFilter"/> + <waitForPageLoad stepKey="waitForFilteredCustomerGridLoad"/> + <click selector="{{AdminOrderCustomersGridSection.firstRow}}" stepKey="clickOnCustomer"/> + <waitForPageLoad stepKey="waitForCreateOrderPageLoad" /> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> + </actionGroup> + <!--Check the required fields are actually required--> <actionGroup name="checkRequiredFieldsNewOrderForm"> <seeElement selector="{{AdminOrderFormAccountSection.requiredGroup}}" stepKey="seeCustomerGroupRequired"/> @@ -84,6 +102,67 @@ <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> </actionGroup> + <!--Add bundle product to order --> + <actionGroup name="addBundleProductToOrder"> + <arguments> + <argument name="product"/> + <argument name="quantity" type="string" defaultValue="1"/> + </arguments> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterBundle"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchBundle"/> + <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectBundleProduct"/> + <waitForElementVisible selector="{{AdminOrderFormBundleProductSection.quantity}}" stepKey="waitForBundleOptionLoad"/> + <wait time="2" stepKey="waitForOptionsToLoad"/> + <fillField selector="{{AdminOrderFormBundleProductSection.quantity}}" userInput="{{quantity}}" stepKey="fillQuantity"/> + <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOk"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addSelected}}" x="0" y="-100" stepKey="scrollToAddSelectedButton"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> + </actionGroup> + + <!--Add downloadable product to order --> + <actionGroup name="addDownloadableProductToOrder"> + <arguments> + <argument name="product"/> + <argument name="link"/> + <argument name="quantity" defaultValue="1" type="string"/> + </arguments> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterDownloadable"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchDownloadable"/> + <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectDownloadableProduct"/> + <waitForElementVisible selector="{{AdminOrderFormDownloadableProductSection.optionSelect(link.title)}}" stepKey="waitForLinkLoad"/> + <click selector="{{AdminOrderFormDownloadableProductSection.optionSelect(link.title)}}" stepKey="selectLink"/> + <fillField selector="{{AdminOrderFormDownloadableProductSection.quantity}}" userInput="{{quantity}}" stepKey="setQuantity"/> + <click selector="{{AdminOrderFormDownloadableProductSection.ok}}" stepKey="confirmConfiguration"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addSelected}}" x="0" y="-100" stepKey="scrollToAddSelectedButton"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> + </actionGroup> + + <!--Add grouped product option to order --> + <actionGroup name="addGroupedProductOptionToOrder"> + <arguments> + <argument name="product"/> + <argument name="option"/> + <argument name="quantity" type="string" defaultValue="1"/> + </arguments> + + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterGrouped"/> + <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchGrouped"/> + <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> + <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectGroupedProduct"/> + <waitForElementVisible selector="{{AdminOrderFormGroupedProductSection.optionQty(option.sku)}}" stepKey="waitForGroupedOptionLoad"/> + <wait time="2" stepKey="waitForOptionsToLoad"/> + <fillField selector="{{AdminOrderFormGroupedProductSection.optionQty(option.sku)}}" userInput="{{quantity}}" stepKey="fillOptionQuantity"/> + <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOk"/> + <scrollTo selector="{{AdminOrderFormItemsSection.addSelected}}" x="0" y="-100" stepKey="scrollToAddSelectedButton"/> + <click selector="{{AdminOrderFormItemsSection.addSelected}}" stepKey="clickAddSelectedProducts"/> + </actionGroup> + <!--Fill customer billing address--> <actionGroup name="fillOrderCustomerInformation"> <arguments> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml index a49d06545df05..ca5e297b72ffb 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminCreditMemoTotalSection.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> <section name="AdminCreditMemoTotalSection"> <element name="subtotalRow" type="text" selector=".order-subtotal-table tbody > tr:nth-of-type({{row}}) td span.price" parameterized="true"/> - <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="total" type="text" selector="//table[contains(@class,'order-subtotal-table')]/tbody/tr/td[contains(text(), '{{total}}')]/following-sibling::td//span[contains(@class, 'price')]" parameterized="true"/> <element name="refundShipping" type="input" selector=".order-subtotal-table tbody input[name='creditmemo[shipping_amount]']"/> <element name="adjustmentRefund" type="input" selector=".order-subtotal-table tbody input[name='creditmemo[adjustment_positive]'"/> <element name="adjustmentFee" type="input" selector=".order-subtotal-table tbody input[name='creditmemo[adjustment_negative]']"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCreditMemosTabSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCreditMemosTabSection.xml index f4835cf02b704..bb0e1618c66e3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCreditMemosTabSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCreditMemosTabSection.xml @@ -12,5 +12,6 @@ <element name="spinner" type="text" selector="[data-role='spinner'][data-component*='sales_order_view_creditmemo']"/> <element name="gridRow" type="text" selector="#sales_order_view_tabs_order_creditmemos_content .data-grid tbody > tr:nth-of-type({{row}})" parameterized="true"/> <element name="viewGridRow" type="button" selector="#sales_order_view_tabs_order_creditmemos_content .data-grid tbody > tr:nth-of-type({{row}}) a[href*='order_creditmemo/view']" parameterized="true"/> + <element name="gridRowCell" type="text" selector="//div[@id='sales_order_view_tabs_order_creditmemos_content']//tr[{{row}}]//td[count(//div[@id='sales_order_view_tabs_order_creditmemos_content']//tr//th[contains(., '{{column}}')][1]/preceding-sibling::th) +1 ]" parameterized="true"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml new file mode 100644 index 0000000000000..c91a1e2aef693 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderCustomersGridSection.xml @@ -0,0 +1,18 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminOrderCustomersGridSection"> + <element name="spinner" type="button" selector=".spinner"/> + <element name="apply" type="button" selector=".action-secondary[title='Search']"/> + <element name="resetFilter" type="button" selector=".action-tertiary[title='Reset Filter']"/> + <element name="emailInput" type="input" selector="#sales_order_create_customer_grid_filter_email"/> + <element name="firstRow" type="button" selector="tr:nth-of-type(1)[data-role='row']"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml new file mode 100644 index 0000000000000..a035e47394d5b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormBundleProductSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminOrderFormBundleProductSection"> + <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> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormDownloadableProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormDownloadableProductSection.xml new file mode 100644 index 0000000000000..b77b01d54f950 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormDownloadableProductSection.xml @@ -0,0 +1,16 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminOrderFormDownloadableProductSection"> + <element name="optionSelect" type="select" selector="//div[contains(@class,'link')]/div/div/input[./../label[contains(text(),{{linkTitle}})]]" 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> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormGroupedProductSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormGroupedProductSection.xml new file mode 100644 index 0000000000000..ceba11f74ae83 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormGroupedProductSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminOrderFormGroupedProductSection"> + <element name="optionQty" type="input" selector="//td[@class='col-sku'][text()='{{productSku}}']/..//input[contains(@class, 'qty')]" parameterized="true"/> + <element name="ok" type="button" selector=".modal-header .page-actions button[data-role='action']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml new file mode 100644 index 0000000000000..2ff3d7756fe36 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -0,0 +1,137 @@ +<?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="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="CreditMemoTotalAfterShippingDiscountTest"> + <annotations> + <features value="Credit memo"/> + <title value="Verify credit memo grand total after shipping discount is applied via Cart Price Rule"/> + <description value="Verify credit memo grand total after shipping discount is applied via Cart Price Rule"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-92924"/> + <group value="sales"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="LoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="SetTaxClassForShipping" stepKey="setShippingTaxClass"/> + </before> + <after> + <actionGroup ref="ResetTaxClassForShipping" stepKey="resetTaxClassForShipping"/> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteSalesRule"> + <argument name="ruleName" value="{{ApiSalesRule.name}}"/> + </actionGroup> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + <deleteData createDataKey="createCategory" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct" stepKey="deleteCategory1"/> + </after> + + <!-- Create a cart price rule for $10 Fixed amount discount --> + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> + <waitForPageLoad stepKey="waitForRulesPage"/> + <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ApiSalesRule.name}}" stepKey="fillRuleName"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="Main Website" stepKey="selectWebsite"/> + <actionGroup ref="selectNotLoggedInCustomerGroup" stepKey="chooseNotLoggedInCustomerGroup"/> + <!--<selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/>--> + + <!-- Open the Actions Tab in the Rules Edit page --> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.applyDiscountToShippingLabel}}" stepKey="waitForElementToBeVisible"/> + <click selector="{{AdminCartPriceRulesFormSection.applyDiscountToShippingLabel}}" stepKey="enableApplyDiscountToShiping"/> + <seeCheckboxIsChecked selector="{{AdminCartPriceRulesFormSection.applyDiscountToShipping}}" stepKey="DiscountIsAppliedToShiping"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="Fixed amount discount" stepKey="selectActionType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="10" stepKey="fillDiscountAmount"/> + + <!--<scrollTo selector="{{AdminCartPriceRulesFormSection.applyDiscountToShippingLabel}}" stepKey="scrollToShippingLabel"/>--> + <!--<click selector="{{AdminCartPriceRulesFormSection.applyDiscountToShippingLabel}}" stepKey="enableApplyDiscountToShiping"/>--> + <!--<seeCheckboxIsChecked selector="{{AdminCartPriceRulesFormSection.applyDiscountToShipping}}" stepKey="DiscountIsAppliedToShiping"/>--> + <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> + <see selector="{{AdminCartPriceRulesSection.messages}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + + <!-- Place an order from Storefront as a Guest --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverOverProduct"/> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductToAdd"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickCart"/> + <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + <!-- fill out customer information --> + <fillField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.street}}" userInput="{{CustomerAddressSimple.street[0]}}" stepKey="enterStreet"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.city}}" userInput="{{CustomerAddressSimple.city}}" stepKey="enterCity"/> + <selectOption selector="{{CheckoutShippingGuestInfoSection.region}}" userInput="{{CustomerAddressSimple.state}}" stepKey="selectRegion"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.postcode}}" userInput="{{CustomerAddressSimple.postcode}}" stepKey="enterPostcode"/> + <fillField selector="{{CheckoutShippingGuestInfoSection.telephone}}" userInput="{{CustomerAddressSimple.telephone}}" stepKey="enterTelephone"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + + <!-- Choose Shippping - Flat Rate Shipping --> + <click selector="{{CheckoutShippingMethodsSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask2"/> + <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNext"/> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" stepKey="waitForPlaceOrderButton"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> + + <!-- Search for Order in the order grid --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <waitForPageLoad time="30" stepKey="waitForOrderListPageLoad"/> + <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilter"/> + <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> + <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearch"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask4"/> + + <!-- Create invoice --> + <click selector="{{AdminOrdersGridSection.firstRow}}" stepKey="clickOrderRow"/> + <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceButton"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> + + <!-- Verify Invoice Totals including subTotal Shipping Discount and GrandTotal --> + <see selector="{{AdminInvoiceTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeInvoiceSubTotal"/> + <comment userInput="Shipping and Handling" stepKey="commentViewShippingAndHandling" after="seeInvoiceSubTotal"/> + <see selector="{{AdminInvoiceTotalSection.total('Shipping')}}" userInput="${{AdminOrderSimpleProduct.shipping}}" stepKey="seeShippingAndHandling"/> + <scrollTo selector="{{AdminInvoiceTotalSection.total('Shipping')}}" stepKey="scrollToInvoiceTotals"/> + <grabTextFrom selector="{{AdminInvoiceTotalSection.total('Shipping')}}" stepKey="grabShippingCost"/> + <assertEquals expected='$5.00' expectedType="string" actual="($grabShippingCost)" message="ExpectedShipping" stepKey="assertShippingAndHandling"/> + + <see selector="{{AdminInvoiceTotalSection.total('Discount')}}" userInput="-$15.00" stepKey="seeShippingAndHandling2"/> + <grabTextFrom selector="{{AdminInvoiceTotalSection.total('Discount')}}" stepKey="grabInvoiceDiscount"/> + <assertEquals expected='-$15.00' expectedType="string" actual="($grabInvoiceDiscount)" message="ExpectedDiscount" stepKey="assertDiscountValue"/> + + <see selector="{{AdminInvoiceTotalSection.grandTotal}}" userInput="$113.00" stepKey="seeCorrectGrandTotal"/> + <grabTextFrom selector="{{AdminInvoiceTotalSection.grandTotal}}" stepKey="grabInvoiceGrandTotal" after="seeCorrectGrandTotal"/> + + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage1"/> + + <!--Create Credit Memo--> + <comment userInput="Admin creates credit memo" stepKey="createCreditMemoComment"/> + <click selector="{{AdminOrderDetailsMainActionsSection.creditMemo}}" stepKey="clickCreateCreditMemo" after="createCreditMemoComment"/> + <seeInCurrentUrl url="{{AdminCreditMemoNewPage.url}}" stepKey="seeNewCreditMemoPage" after="clickCreateCreditMemo"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Memo" stepKey="seeNewMemoInPageTitle" after="seeNewCreditMemoPage"/> + + <!-- Verify Refund Totals --> + <see selector="{{AdminCreditMemoTotalSection.total('Subtotal')}}" userInput="${{AdminOrderSimpleProduct.subtotal}}" stepKey="seeRefundSubTotal"/> + <grabTextFrom selector="{{AdminCreditMemoTotalSection.total('Discount')}}" stepKey="grabRefundDiscountValue"/> + <assertEquals expected='-$15.00' expectedType="string" actual="($grabRefundDiscountValue)" message="notExpectedDiscountOnRefundPage" stepKey="assertDiscountValue1"/> + <grabTextFrom selector="{{AdminInvoiceTotalSection.grandTotal}}" stepKey="grabRefundGrandTotal"/> + <assertEquals expected="($grabInvoiceGrandTotal)" actual="($grabRefundGrandTotal)" message="RefundGrandTotalMatchesWithInvoiceGrandTotal" stepKey="compareRefundGrandTotalAndInvoiceGrandTotal"/> + </test> +</tests> + diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Create/TotalsTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Create/TotalsTest.php index 2a839dd018dba..492cfee5f5d83 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Create/TotalsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Create/TotalsTest.php @@ -70,10 +70,10 @@ protected function setUp() $this->quoteMock->expects($this->any()) ->method('getBillingAddress') - ->willreturn($this->billingAddressMock); + ->willReturn($this->billingAddressMock); $this->quoteMock->expects($this->any()) ->method('getShippingAddress') - ->willreturn($this->shippingAddressMock); + ->willReturn($this->shippingAddressMock); $this->sessionQuoteMock->expects($this->any())->method('getQuote')->willReturn($this->quoteMock); $this->totals = $this->helperManager->getObject( \Magento\Sales\Block\Adminhtml\Order\Create\Totals::class, @@ -88,7 +88,7 @@ public function testGetTotals($isVirtual) { $expected = 'expected'; $this->quoteMock->expects($this->at(1))->method('collectTotals'); - $this->quoteMock->expects($this->once())->method('isVirtual')->willreturn($isVirtual); + $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn($isVirtual); if ($isVirtual) { $this->billingAddressMock->expects($this->once())->method('getTotals')->willReturn($expected); } else { diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php index 2531a26321d67..18efef38b204c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Total/DiscountTest.php @@ -48,7 +48,7 @@ protected function setUp() ]); $this->creditmemoMock = $this->createPartialMock(\Magento\Sales\Model\Order\Creditmemo::class, [ 'setBaseCost', 'getAllItems', 'getOrder', 'getBaseShippingAmount', 'roundPrice', - 'setDiscountAmount', 'setBaseDiscountAmount' + 'setDiscountAmount', 'setBaseDiscountAmount', 'getBaseShippingInclTax', 'getBaseShippingTaxAmount' ]); $this->creditmemoItemMock = $this->createPartialMock(\Magento\Sales\Model\Order\Creditmemo\Item::class, [ 'getHasChildren', 'getBaseCost', 'getQty', 'getOrderItem', 'setDiscountAmount', @@ -127,6 +127,82 @@ public function testCollect() $this->assertEquals($this->total, $this->total->collect($this->creditmemoMock)); } + public function testCollectNoBaseShippingAmount() + { + $this->creditmemoMock->expects($this->exactly(2)) + ->method('setDiscountAmount') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->exactly(2)) + ->method('setBaseDiscountAmount') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->once()) + ->method('getOrder') + ->willReturn($this->orderMock); + $this->creditmemoMock->expects($this->once()) + ->method('getBaseShippingAmount') + ->willReturn(0); + $this->creditmemoMock->expects($this->once()) + ->method('getBaseShippingInclTax') + ->willReturn(1); + $this->creditmemoMock->expects($this->once()) + ->method('getBaseShippingTaxAmount') + ->willReturn(0); + $this->orderMock->expects($this->once()) + ->method('getBaseShippingDiscountAmount') + ->willReturn(1); + $this->orderMock->expects($this->exactly(2)) + ->method('getBaseShippingAmount') + ->willReturn(1); + $this->orderMock->expects($this->once()) + ->method('getShippingAmount') + ->willReturn(1); + $this->creditmemoMock->expects($this->once()) + ->method('getAllItems') + ->willReturn([$this->creditmemoItemMock]); + $this->creditmemoItemMock->expects($this->atLeastOnce()) + ->method('getOrderItem') + ->willReturn($this->orderItemMock); + $this->orderItemMock->expects($this->once()) + ->method('isDummy') + ->willReturn(false); + $this->orderItemMock->expects($this->once()) + ->method('getDiscountInvoiced') + ->willReturn(1); + $this->orderItemMock->expects($this->once()) + ->method('getBaseDiscountInvoiced') + ->willReturn(1); + $this->orderItemMock->expects($this->once()) + ->method('getQtyInvoiced') + ->willReturn(1); + $this->orderItemMock->expects($this->once()) + ->method('getDiscountRefunded') + ->willReturn(1); + $this->orderItemMock->expects($this->once()) + ->method('getQtyRefunded') + ->willReturn(0); + $this->creditmemoItemMock->expects($this->once()) + ->method('isLast') + ->willReturn(false); + $this->creditmemoItemMock->expects($this->atLeastOnce()) + ->method('getQty') + ->willReturn(1); + $this->creditmemoItemMock->expects($this->exactly(1)) + ->method('setDiscountAmount') + ->willReturnSelf(); + $this->creditmemoItemMock->expects($this->exactly(1)) + ->method('setBaseDiscountAmount') + ->willReturnSelf(); + $this->creditmemoMock->expects($this->exactly(2)) + ->method('roundPrice') + ->willReturnMap( + [ + [1, 'regular', true, 1], + [1, 'base', true, 1] + ] + ); + $this->assertEquals($this->total, $this->total->collect($this->creditmemoMock)); + } + public function testCollectZeroShipping() { $this->creditmemoMock->expects($this->exactly(2)) diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totalbar.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totalbar.phtml index b038c72199c7f..af7dfbc4e2ab3 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totalbar.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totalbar.phtml @@ -6,14 +6,15 @@ // @codingStandardsIgnoreFile +$totals = $block->getTotals(); ?> -<?php if (sizeof($block->getTotals()) > 0): ?> +<?php if ($totals && count($totals) > 0): ?> <table class="items-to-invoice"> <tr> - <?php foreach ($block->getTotals() as $_total): ?> - <td <?php if ($_total['grand']): ?>class="grand-total"<?php endif; ?>> - <?= /* @escapeNotVerified */ $_total['label'] ?><br /> - <?= /* @escapeNotVerified */ $_total['value'] ?> + <?php foreach ($totals as $total): ?> + <td <?php if ($total['grand']): ?>class="grand-total"<?php endif; ?>> + <?= $block->escapeHtml($total['label']) ?><br /> + <?= $block->escapeHtml($total['value']) ?> </td> <?php endforeach; ?> </tr> diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php b/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php index 794fc94d6a2a8..3a5ed16fdd2fd 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Rule.php @@ -239,7 +239,7 @@ public function saveStoreLabels($ruleId, $labels) $connection->delete($table, ['rule_id=?' => $ruleId, 'store_id IN (?)' => $deleteByStoreIds]); } } catch (\Exception $e) { - $connection->rollback(); + $connection->rollBack(); throw $e; } $connection->commit(); diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index 87408aac2c0c9..f31ff1a456898 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -25,6 +25,8 @@ <!-- Actions sub-form --> <element name="actionsHeader" type="button" selector="div[data-index='actions']" timeout="30"/> <element name="apply" type="select" selector="select[name='simple_action']"/> + <element name="applyDiscountToShipping" type="checkbox" selector="input[name='apply_to_shipping']"/> + <element name="applyDiscountToShippingLabel" type="checkbox" selector="input[name='apply_to_shipping']+label"/> <element name="discountAmount" type="input" selector="input[name='discount_amount']"/> <element name="discountStep" type="input" selector="input[name='discount_step']"/> diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml index a1e15582b450d..543725fc5fa1c 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml @@ -10,6 +10,6 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> <section name="StorefrontQuickSearchSection"> <element name="searchPhrase" type="input" selector="#search"/> - <element name="searchButton" type="button" selector="button.action.search"/> + <element name="searchButton" type="button" selector="button.action.search" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging/Grid.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging/Grid.php index 9e340cc31ff17..1d3f6ad1ee5a3 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging/Grid.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging/Grid.php @@ -10,7 +10,7 @@ class Grid extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'order/packaging/grid.phtml'; + protected $_template = 'Magento_Shipping::order/packaging/grid.phtml'; /** * Core registry diff --git a/app/code/Magento/Shipping/Block/Order/Shipment.php b/app/code/Magento/Shipping/Block/Order/Shipment.php index 653fb357f0b1d..21e960985d6b6 100644 --- a/app/code/Magento/Shipping/Block/Order/Shipment.php +++ b/app/code/Magento/Shipping/Block/Order/Shipment.php @@ -18,7 +18,7 @@ class Shipment extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'order/shipment.phtml'; + protected $_template = 'Magento_Shipping::order/shipment.phtml'; /** * Core registry diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index 5575792c346d3..6763046373ce7 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -329,11 +329,24 @@ public function checkAvailableShipCountries(\Magento\Framework\DataObject $reque * @return $this|bool|\Magento\Framework\DataObject * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + public function processAdditionalValidation(\Magento\Framework\DataObject $request) { return $this; } + /** + * Processing additional validation to check is carrier applicable. + * + * @param \Magento\Framework\DataObject $request + * @return $this|bool|\Magento\Framework\DataObject + * @deprecated + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + { + return $this->processAdditionalValidation($request); + } + /** * Determine whether current carrier enabled for activity * diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index 8244fcc4bad9d..be2588dc48711 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -302,10 +302,24 @@ public function getAllItems(RateRequest $request) * * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject + * @deprecated * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) + { + return $this->processAdditionalValidation($request); + } + + /** + * Processing additional validation to check if carrier applicable. + * + * @param \Magento\Framework\DataObject $request + * @return $this|bool|\Magento\Framework\DataObject + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function processAdditionalValidation(\Magento\Framework\DataObject $request) { //Skip by item validation if there is no items in request if (!count($this->getAllItems($request))) { diff --git a/app/code/Magento/Shipping/Model/Shipping.php b/app/code/Magento/Shipping/Model/Shipping.php index 2223cb8ae3bf2..57e055e83a58a 100644 --- a/app/code/Magento/Shipping/Model/Shipping.php +++ b/app/code/Magento/Shipping/Model/Shipping.php @@ -259,7 +259,7 @@ public function collectCarrierRates($carrierCode, $request) $carrier->setActiveFlag($this->_availabilityConfigField); $result = $carrier->checkAvailableShipCountries($request); if (false !== $result && !$result instanceof \Magento\Quote\Model\Quote\Address\RateResult\Error) { - $result = $carrier->proccessAdditionalValidation($request); + $result = $carrier->processAdditionalValidation($request); } /* * Result will be false if the admin set not to show the shipping module diff --git a/app/code/Magento/Shipping/Model/Shipping/Labels.php b/app/code/Magento/Shipping/Model/Shipping/Labels.php index 5c796d9fa6897..08ce168567182 100644 --- a/app/code/Magento/Shipping/Model/Shipping/Labels.php +++ b/app/code/Magento/Shipping/Model/Shipping/Labels.php @@ -117,8 +117,8 @@ public function requestToShipment(Shipment $orderShipment) ) ); - if (!$admin->getFirstname() - || !$admin->getLastname() + if (!$admin->getFirstName() + || !$admin->getLastName() || !$storeInfo->getName() || !$storeInfo->getPhone() || !$originStreet1 @@ -188,8 +188,8 @@ protected function setShipperDetails( ); $request->setShipperContactPersonName($storeAdmin->getName()); - $request->setShipperContactPersonFirstName($storeAdmin->getFirstname()); - $request->setShipperContactPersonLastName($storeAdmin->getLastname()); + $request->setShipperContactPersonFirstName($storeAdmin->getFirstName()); + $request->setShipperContactPersonLastName($storeAdmin->getLastName()); $request->setShipperContactCompanyName($store->getName()); $request->setShipperContactPhoneNumber($store->getPhone()); $request->setShipperEmail($storeAdmin->getEmail()); diff --git a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml index 7c936a7420db9..30f508beb57ab 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Section/AdminShipmentItemsSection.xml @@ -9,10 +9,10 @@ <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"> <section name="AdminShipmentItemsSection"> - <element name="itemName" type="text" selector=".order-shipment-table tbody:nth-of-type({{row}}) .col-product .product-title" parameterized="true"/> - <element name="itemSku" type="text" selector=".order-shipment-table tbody:nth-of-type({{row}}) .col-product .product-sku-block" parameterized="true"/> - <element name="itemQty" type="text" selector=".order-shipment-table tbody:nth-of-type({{row}}) .col-ordered-qty .qty-table" parameterized="true"/> - <element name="itemQtyToShip" type="input" selector=".order-shipment-table tbody:nth-of-type({{row}}) .col-qty input.qty-item"/> + <element name="itemName" type="text" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-product .product-title" parameterized="true"/> + <element name="itemSku" type="text" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-product .product-sku-block" parameterized="true"/> + <element name="itemQty" type="text" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-ordered-qty .qty-table" parameterized="true"/> + <element name="itemQtyToShip" type="input" selector=".order-shipment-table tbody:nth-of-type({{var1}}) .col-qty input.qty-item" parameterized="true"/> <element name="nameColumn" type="text" selector=".order-shipment-table .col-product .product-title"/> <element name="skuColumn" type="text" selector=".order-shipment-table .col-product .product-sku-block"/> </section> diff --git a/app/code/Magento/Shipping/Test/Unit/Model/Carrier/AbstractCarrierOnlineTest.php b/app/code/Magento/Shipping/Test/Unit/Model/Carrier/AbstractCarrierOnlineTest.php index 6f87eb171a398..b40f5b26b89f1 100644 --- a/app/code/Magento/Shipping/Test/Unit/Model/Carrier/AbstractCarrierOnlineTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Model/Carrier/AbstractCarrierOnlineTest.php @@ -100,7 +100,7 @@ public function testComposePackages() $this->stockItemData->expects($this->atLeastOnce())->method('getIsDecimalDivided') ->will($this->returnValue(true)); - $this->carrier->proccessAdditionalValidation($request); + $this->carrier->processAdditionalValidation($request); } public function testParseXml() diff --git a/app/code/Magento/Signifyd/Block/Fingerprint.php b/app/code/Magento/Signifyd/Block/Fingerprint.php index db76fc6c94468..f43bffce1fc1a 100644 --- a/app/code/Magento/Signifyd/Block/Fingerprint.php +++ b/app/code/Magento/Signifyd/Block/Fingerprint.php @@ -42,7 +42,7 @@ class Fingerprint extends Template * @var string * @since 100.2.0 */ - protected $_template = 'fingerprint.phtml'; + protected $_template = 'Magento_Signifyd::fingerprint.phtml'; /** * @param Context $context diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php index f0be0fe7ab682..422fed9d9a688 100644 --- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php +++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Delete.php @@ -53,9 +53,13 @@ public function execute() $sitemap->load($id); // delete file $sitemapPath = $sitemap->getSitemapPath(); + if ($sitemapPath && $sitemapPath[0] === DIRECTORY_SEPARATOR) { + $sitemapPath = mb_substr($sitemapPath, 1); + } $sitemapFilename = $sitemap->getSitemapFilename(); - - $path = $directory->getRelativePath($sitemapPath . $sitemapFilename); + $path = $directory->getRelativePath( + $sitemapPath .$sitemapFilename + ); if ($sitemap->getSitemapFilename() && $directory->isFile($path)) { $directory->delete($path); } diff --git a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php b/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php deleted file mode 100644 index ed004fe88b318..0000000000000 --- a/app/code/Magento/Sitemap/Test/Unit/Controller/Adminhtml/Sitemap/DeleteTest.php +++ /dev/null @@ -1,149 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Sitemap\Test\Unit\Controller\Adminhtml\Sitemap; - -class DeleteTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var \Magento\Sitemap\Controller\Adminhtml\Sitemap\Delete - */ - private $deleteController; - - /** - * @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject - */ - private $filesystemMock; - - /** - * @var \Magento\Sitemap\Model\SitemapFactory|\PHPUnit_Framework_MockObject_MockObject - */ - private $sitemapFactoryMock; - - /** - * @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $requestMock; - - /** - * @var \Magento\Framework\App\ResponseInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $responseMock; - - /** - * @var \Magento\Framework\Message\ManagerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - private $messageManagerMock; - - protected function setUp() - { - $this->filesystemMock = $this->getMockBuilder(\Magento\Framework\Filesystem::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMock(); - $this->sitemapFactoryMock = $this->getMockBuilder(\Magento\Sitemap\Model\SitemapFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->requestMock = $this->getMockBuilder(\Magento\Framework\App\RequestInterface::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMockForAbstractClass(); - $this->responseMock = $this->getMockBuilder(\Magento\Framework\App\ResponseInterface::class) - ->disableOriginalConstructor() - ->setMethods(['setRedirect']) - ->getMockForAbstractClass(); - $this->messageManagerMock = $this->getMockBuilder(\Magento\Framework\Message\ManagerInterface::class) - ->disableOriginalConstructor() - ->setMethods([]) - ->getMockForAbstractClass(); - - $objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - - $this->deleteController = $objectManagerHelper->getObject( - \Magento\Sitemap\Controller\Adminhtml\Sitemap\Delete::class, - [ - 'request' => $this->requestMock, - 'response' => $this->responseMock, - 'messageManager' => $this->messageManagerMock, - 'filesystem' => $this->filesystemMock, - 'sitemapFactory' => $this->sitemapFactoryMock - ] - ); - } - - public function testExecuteWithoutSitemapId() - { - $writeDirectoryMock = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\WriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); - $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn(0); - $this->responseMock->expects($this->once())->method('setRedirect'); - $this->messageManagerMock->expects($this->any()) - ->method('addError') - ->with('We can\'t find a sitemap to delete.'); - - $this->deleteController->execute(); - } - - public function testExecuteCannotFindSitemap() - { - $id = 1; - $writeDirectoryMock = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\WriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); - $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn($id); - - $sitemapMock = $this->getMockBuilder(\Magento\Sitemap\Model\Sitemap::class) - ->disableOriginalConstructor() - ->setMethods(['load']) - ->getMock(); - - $sitemapMock->expects($this->once())->method('load')->with($id)->willThrowException(new \Exception); - $this->sitemapFactoryMock->expects($this->once())->method('create')->willReturn($sitemapMock); - $this->responseMock->expects($this->once())->method('setRedirect'); - $this->messageManagerMock->expects($this->any()) - ->method('addError'); - - $this->deleteController->execute(); - } - - public function testExecute() - { - $id = 1; - $sitemapPath = '/'; - $sitemapFilename = 'sitemap.xml'; - $relativePath = '/sitemap.xml'; - $writeDirectoryMock = $this->getMockBuilder(\Magento\Framework\Filesystem\Directory\WriteInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($writeDirectoryMock); - $this->requestMock->expects($this->any())->method('getParam')->with('sitemap_id')->willReturn($id); - - $sitemapMock = $this->getMockBuilder(\Magento\Sitemap\Model\Sitemap::class) - ->disableOriginalConstructor() - ->setMethods(['getSitemapPath', 'getSitemapFilename', 'load', 'delete']) - ->getMock(); - $sitemapMock->expects($this->once())->method('load')->with($id); - $sitemapMock->expects($this->once())->method('delete'); - $sitemapMock->expects($this->any())->method('getSitemapPath')->willReturn($sitemapPath); - $sitemapMock->expects($this->any())->method('getSitemapFilename')->willReturn($sitemapFilename); - $this->sitemapFactoryMock->expects($this->once())->method('create')->willReturn($sitemapMock); - $writeDirectoryMock->expects($this->any()) - ->method('getRelativePath') - ->with($sitemapPath . $sitemapFilename) - ->willReturn($relativePath); - $writeDirectoryMock->expects($this->once())->method('isFile')->with($relativePath)->willReturn(true); - $writeDirectoryMock->expects($this->once())->method('delete')->with($relativePath)->willReturn(true); - - $this->responseMock->expects($this->once())->method('setRedirect'); - $this->messageManagerMock->expects($this->any()) - ->method('addSuccess'); - - $this->deleteController->execute(); - } -} diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreGroupActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreGroupActionGroup.xml new file mode 100644 index 0000000000000..0819a74ea8996 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreGroupActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateStoreGroupActionGroup"> + <arguments> + <argument name="Website" defaultValue="_defaultWebsite"/> + </arguments> + <amOnPage url="{{AdminSystemStoreGroupPage.url}}" stepKey="navigateToNewStoreGroup"/> + <waitForPageLoad stepKey="waitForStoreGroupPageLoad" /> + + <comment userInput="Creating Store Group" stepKey="storeGroupCreationComment" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeGrpWebsiteDropdown}}" userInput="{{Website.name}}" stepKey="selectWebsite" /> + <fillField selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" userInput="{{CustomStoreGroupCustomWebsite.name}}" stepKey="enterStoreGroupName" /> + <fillField selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" userInput="{{CustomStoreGroupCustomWebsite.code}}" stepKey="enterStoreGroupCode" /> + <selectOption selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" userInput="Default Category" stepKey="setRootCategory" /> + <click selector="{{AdminNewStoreGroupActionsSection.saveButton}}" stepKey="clickSaveStoreGroup" /> + <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReload"/> + <see userInput="You saved the store." stepKey="seeSavedMessage" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml new file mode 100644 index 0000000000000..0f8673eb2f4aa --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/DeleteCustomWebsiteActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + <actionGroup name="DeleteCustomWebsiteActionGroup"> + <arguments> + <argument name="websiteName" defaultValue="customWebsite.name"/> + </arguments> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnTheStorePage"/> + <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> + <waitForPageLoad stepKey="waitForPageLoadAfterResetButtonClicked" time="10"/> + <fillField userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> + <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton" /> + <waitForPageLoad stepKey="waitForPageLoadAfterSearch" time="10"/> + <see userInput="{{websiteName}}" selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="verifyThatCorrectWebsiteFound"/> + <click selector="{{AdminStoresGridSection.websiteNameInFirstRow}}" stepKey="clickEditExistingWebsite"/> + + <click selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="clickDeleteWebsiteButtonOnEditStorePage"/> + <selectOption userInput="No" selector="{{AdminStoresDeleteWebsiteSection.createDbBackup}}" stepKey="setCreateDbBackupToNo"/> + <click selector="{{AdminStoresDeleteWebsiteSection.deleteButton}}" stepKey="clickDeleteButtonOnDeleteWebsitePage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml index 036069977bf52..4e3c724572e79 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -39,4 +39,14 @@ <data key="store_type">group</data> <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> </entity> + <entity name="staticStore" type="store"> + <!--data key="group_id">customStoreGroup.id</data--> + <data key="name" >Second Store View</data> + <data key="code" >store123</data> + <data key="is_active">1</data> + <data key="store_id">null</data> + <data key="store_action">add</data> + <data key="store_type">group</data> + <requiredEntity type="storeGroup">customStoreGroup</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml index 50139a0a87862..8c293bc22f2e8 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreGroupData.xml @@ -21,4 +21,10 @@ <data key="store_action">add</data> <data key="store_type">group</data> </entity> + <entity name="staticStoreGroup" type="group"> + <data key="name">NewStore</data> + <data key="code">Base12</data> + <data key="root_category_id">2</data> + <data key="website_id">1</data> + </entity> </entities> diff --git a/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml new file mode 100644 index 0000000000000..e8528fba1ae29 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Data/WebsiteData.xml @@ -0,0 +1,22 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="_defaultWebsite" type="website"> + <data key="name">Main Website</data> + <data key="code">base</data> + <data key="sort_order">0</data> + </entity> + <entity name="customWebsite" type="website"> + <data key="name" unique="suffix">Second Website</data> + <data key="code" unique="suffix">second_website</data> + <data key="sort_order">10</data> + <data key="store_action">add</data> + <data key="store_type">website</data> + <data key="website_id">null</data> + </entity> +</entities> diff --git a/app/code/Magento/Store/Test/Mftf/Metadata/website-meta.xml b/app/code/Magento/Store/Test/Mftf/Metadata/website-meta.xml new file mode 100644 index 0000000000000..4e314396ab046 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Metadata/website-meta.xml @@ -0,0 +1,22 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateWebsite" dataType="website" type="create" + auth="adminFormKey" url="/admin/system_store/save" method="POST" successRegex="/messages-message-success/" returnRegex=""> + <object dataType="website" key="website"> + <field key="website_id">string</field> + <field key="name">string</field> + <field key="code">string</field> + <field key="sort_order">integer</field> + </object> + <field key="store_action">string</field> + <field key="store_type">string</field> + </operation> +</operations> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupActionsSection.xml new file mode 100644 index 0000000000000..f026c7765b6d6 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreGroupActionsSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewStoreGroupActionsSection"> + <element name="backButton" type="button" selector="#back" timeout="30"/> + <element name="delete" type="button" selector="#delete" timeout="30"/> + <element name="resetButton" type="button" selector="#reset" timeout="30"/> + <element name="saveButton" type="button" selector="#save" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml index 5f8e27f3ec101..a3b5d1e616319 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewStoreViewActionsSection.xml @@ -10,6 +10,6 @@ <element name="backButton" type="button" selector="#back" timeout="30"/> <element name="delete" type="button" selector="#delete" timeout="30"/> <element name="resetButton" type="button" selector="#reset" timeout="30"/> - <element name="saveButton" type="button" selector="#save" timeout="30"/> + <element name="saveButton" type="button" selector="#save" timeout="60"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml index 8048ded87e260..703abea8cfd0d 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminNewWebsiteActionsSection.xml @@ -7,6 +7,6 @@ --> <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"> <section name="AdminNewWebsiteActionsSection"> - <element name="saveWebsite" type="button" selector="#save" timeout="30"/> + <element name="saveWebsite" type="button" selector="#save" timeout="60"/> </section> </sections> \ No newline at end of file diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml index ae0283865b774..6dc766c0c02da 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoreGroupActionsSection.xml @@ -7,6 +7,6 @@ --> <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"> <section name="AdminStoreGroupActionsSection"> - <element name="saveButton" type="button" selector="#save" timeout="30" /> + <element name="saveButton" type="button" selector="#save" timeout="60" /> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml new file mode 100644 index 0000000000000..50c536dcfe809 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresDeleteWebsiteSection.xml @@ -0,0 +1,13 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminStoresDeleteWebsiteSection"> + <element name="createDbBackup" type="select" selector="#store_create_backup"/> + <element name="deleteButton" type="button" selector="#delete" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml new file mode 100644 index 0000000000000..09137a7003b94 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddSwatchToProductActionGroup.xml @@ -0,0 +1,65 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> + + <actionGroup name="AddVisualSwatchToProductActionGroup"> + <arguments> + <argument name="attribute" defaultValue="visualSwatchAttribute"/> + <argument name="option1" defaultValue="visualSwatchOption1"/> + <argument name="option2" defaultValue="visualSwatchOption2"/> + </arguments> + + <seeInCurrentUrl url="{{ProductCatalogPage.url}}" stepKey="seeOnProductEditPage"/> + <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="openConfigurationPanel"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="waitForSlideOut"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickCreateNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{attribute.default_label}}" stepKey="fillDefaultLabel"/> + <selectOption selector="{{AdminNewAttributePanel.inputType}}" userInput="{{attribute.input_type}}" stepKey="selectInputType"/> + <!--Add swatch options--> + <click selector="{{AdminNewAttributePanel.addVisualSwatchOption}}" stepKey="clickAddSwatch1"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.visualSwatchOptionAdminValue('0')}}" stepKey="waitForOption1Row"/> + <fillField selector="{{AdminNewAttributePanel.visualSwatchOptionAdminValue('0')}}" userInput="{{option1.admin_label}}" stepKey="fillAdminLabel1"/> + <fillField selector="{{AdminNewAttributePanel.visualSwatchOptionDefaultStoreValue('0')}}" userInput="{{option1.default_label}}" stepKey="fillDefaultStoreLabel1"/> + <click selector="{{AdminNewAttributePanel.addVisualSwatchOption}}" stepKey="clickAddSwatch2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.visualSwatchOptionAdminValue('1')}}" stepKey="waitForOption2Row"/> + <fillField selector="{{AdminNewAttributePanel.visualSwatchOptionAdminValue('1')}}" userInput="{{option2.admin_label}}" stepKey="fillAdminLabel2"/> + <fillField selector="{{AdminNewAttributePanel.visualSwatchOptionDefaultStoreValue('1')}}" userInput="{{option2.default_label}}" stepKey="fillDefaultStoreLabel2"/> + + <!--Save attribute--> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickOnNewAttributePanel"/> + <waitForPageLoad stepKey="waitForSaveAttribute"/> + <switchToIFrame stepKey="switchOutOfIFrame"/> + + <!--Find attribute in grid and select--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.attributeCodeFilterInput}}" userInput="{{attribute.default_label}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(attribute.default_label)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </actionGroup> + +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml b/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml new file mode 100644 index 0000000000000..0e70bdcc70249 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Data/SwatchAttributeData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="visualSwatchAttribute" type="SwatchAttribute"> + <data key="default_label" unique="suffix">VisualSwatchAttr</data> + <data key="input_type">Visual Swatch</data> + </entity> +</entities> diff --git a/app/code/Magento/Swatches/Test/Mftf/Data/SwatchOptionData.xml b/app/code/Magento/Swatches/Test/Mftf/Data/SwatchOptionData.xml new file mode 100644 index 0000000000000..76bfbe8e1b870 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Data/SwatchOptionData.xml @@ -0,0 +1,20 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="visualSwatchOption1" type="SwatchOption"> + <data key="admin_label" unique="suffix">VisualOpt1</data> + <data key="default_label" unique="suffix">VisualOpt1</data> + </entity> + + <entity name="visualSwatchOption2" type="SwatchOption"> + <data key="admin_label" unique="suffix">VisualOpt2</data> + <data key="default_label" unique="suffix">VisualOpt2</data> + </entity> +</entities> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml new file mode 100644 index 0000000000000..36c2056a45771 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Section/AdminNewAttributePanelSection.xml @@ -0,0 +1,17 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminNewAttributePanel"> + <element name="addVisualSwatchOption" type="button" selector="button#add_new_swatch_visual_option_button"/> + <element name="addTextSwatchOption" type="button" selector="button#add_new_swatch_text_option_button"/> + <element name="visualSwatchOptionAdminValue" type="input" selector="[data-role='swatch-visual-options-container'] input[name='optionvisual[value][option_{{row}}][0]']" parameterized="true"/> + <element name="visualSwatchOptionDefaultStoreValue" type="input" selector="[data-role='swatch-visual-options-container'] input[name='optionvisual[value][option_{{row}}][1]']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml new file mode 100644 index 0000000000000..4da84f92e20ed --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="StorefrontProductInfoMainSection"> + <element name="swatchOptionByLabel" type="button" selector="div.swatch-option[option-label={{opt}}]" parameterized="true"/> + <element name="selectedSwatchValue" type="text" selector="//div[contains(@class, 'swatch-attribute') and contains(., '{{attr}}')]//span[contains(@class, 'swatch-attribute-selected-option')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml new file mode 100644 index 0000000000000..cfd220d12b1d7 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml @@ -0,0 +1,92 @@ +<?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="../../../../../../vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/testSchema.xsd"> + <test name="StorefrontSwatchProductWithFileCustomOptionTest"> + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Add configurable product to cart"/> + <title value="Correct error message and redirect with invalid file option"/> + <description value="Configurable product with swatch option and file custom option. When adding to cart with an invalid filetype, the correct error message is shown, and options remain selected."/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-93101"/> + <group value="ConfigurableProduct"/> + <group value="Swatches"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> + </after> + + <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> + + <!-- Create a configurable swatch product via the UI --> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> + <waitForPageLoad stepKey="waitForProductPage"/> + <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="AddVisualSwatchToProductActionGroup" stepKey="addSwatchToProduct"/> + <!--Add custom option to configurable product--> + <actionGroup ref="AddProductCustomOptionFile" stepKey="addCustomOptionToProduct"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + + <!--Go to storefront--> + <amOnPage url="" stepKey="goToHomePage"/> + <waitForPageLoad stepKey="waitForHomePageLoad"/> + <click selector="{{StorefrontNavigationSection.topCategory($$createCategory.name$$)}}" stepKey="goToCategoryStorefront"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="$$createCategory.name$$" stepKey="seeOnCategoryPage"/> + <!--Add configurable product to cart--> + <moveMouseOver selector="{{StorefrontCategoryProductSection.ProductTitleByName(BaseConfigurableProduct.name)}}" stepKey="hoverProductInGrid"/> + <click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(BaseConfigurableProduct.name)}}" stepKey="tryAddToCartFromCategoryPage"/> + <waitForPageLoad stepKey="waitForRedirectToProductPage"/> + <seeInCurrentUrl url="{{StorefrontProductPage.url(BaseConfigurableProduct.name)}}" stepKey="seeOnProductPage"/> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel(visualSwatchOption2.default_label)}}" stepKey="clickSwatchOption"/> + <see selector="{{StorefrontProductInfoMainSection.selectedSwatchValue(visualSwatchAttribute.default_label)}}" userInput="{{visualSwatchOption2.default_label}}" stepKey="seeSwatchIsSelected"/> + + <!--Try invalid file--> + <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" userInput="lorem_ipsum.docx" stepKey="attachInvalidFile"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCartInvalidFile"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.alertMessage}}" stepKey="waitForErrorMessageInvalidFile"/> + <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="The file 'lorem_ipsum.docx' for '{{ProductOptionFile.title}}' has an invalid extension." stepKey="seeMessageInvalidFile"/> + <!--Swatch remains selected--> + <see selector="{{StorefrontProductInfoMainSection.selectedSwatchValue(visualSwatchAttribute.default_label)}}" userInput="{{visualSwatchOption2.default_label}}" stepKey="seeSwatchRemainsSelected"/> + <!--Try valid file--> + <attachFile selector="{{StorefrontProductInfoMainSection.addLinkFileUploadFile(ProductOptionFile.title)}}" userInput="{{MagentoLogo.file}}" stepKey="attachValidFile"/> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$132.99" stepKey="seePriceUpdated"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="addToCartValidFile"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" stepKey="waitForSuccessMessage"/> + <see selector="{{StorefrontProductPageSection.messagesBlock}}" userInput="You added {{BaseConfigurableProduct.name}} to your shopping cart." stepKey="seeSuccessMessage"/> + + <!--Check item in cart--> + <amOnPage url="{{CheckoutCartPage.url}}" stepKey="goToCart"/> + <waitForPageLoad stepKey="waitForCartPageLoad"/> + <seeElement selector="{{CheckoutCartProductSection.ProductLinkByName(BaseConfigurableProduct.name)}}" stepKey="seeProductInCart"/> + <see selector="{{CheckoutCartProductSection.ProductOptionByNameAndAttribute(BaseConfigurableProduct.name, visualSwatchAttribute.default_label)}}" userInput="{{visualSwatchOption2.default_label}}" stepKey="seeSelectedSwatch"/> + <see selector="{{CheckoutCartProductSection.ProductOptionByNameAndAttribute(BaseConfigurableProduct.name, ProductOptionFile.title)}}" userInput="{{MagentoLogo.file}}" stepKey="seeCorrectOptionFile"/> + <!--Delete cart item--> + <click selector="{{CheckoutCartProductSection.RemoveItem}}" stepKey="deleteCartItem"/> + + <!--Delete product--> + <actionGroup ref="deleteProductBySku" stepKey="deleteProduct"> + <argument name="sku" value="{{BaseConfigurableProduct.sku}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml index fc1de530a66bd..b044e692313dc 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml @@ -20,6 +20,9 @@ "gallerySwitchStrategy": "<?php /* @escapeNotVerified */ echo $block->getVar('gallery_switch_strategy', 'Magento_ConfigurableProduct') ?: 'replace'; ?>" } + }, + "*" : { + "Magento_Swatches/js/catalog-add-to-cart": {} } } </script> diff --git a/app/code/Magento/Swatches/view/frontend/web/js/catalog-add-to-cart.js b/app/code/Magento/Swatches/view/frontend/web/js/catalog-add-to-cart.js index d699faae3a85f..80003ef68f1c4 100644 --- a/app/code/Magento/Swatches/view/frontend/web/js/catalog-add-to-cart.js +++ b/app/code/Magento/Swatches/view/frontend/web/js/catalog-add-to-cart.js @@ -7,11 +7,23 @@ require([ ], function ($) { 'use strict'; + /** + * Add selected swatch attributes to redirect url + * + * @see Magento_Catalog/js/catalog-add-to-cart + */ $('body').on('catalogCategoryAddToCartRedirect', function (event, data) { $(data.form).find('[name*="super"]').each(function (index, item) { - var $item = $(item); + var $item = $(item), + attr; + + if ($item.attr('data-attr-name')) { + attr = $item.attr('data-attr-name'); + } else { + attr = $item.parent().attr('attribute-code'); + } + data.redirectParameters.push(attr + '=' + $item.val()); - data.redirectParameters.push($item.attr('data-attr-name') + '=' + $item.val()); }); }); }); 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 a76f070500797..4a57bf796aabd 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 @@ -1214,8 +1214,20 @@ define([ */ _EmulateSelected: function (selectedAttributes) { $.each(selectedAttributes, $.proxy(function (attributeCode, optionId) { - this.element.find('.' + this.options.classes.attributeClass + - '[attribute-code="' + attributeCode + '"] [option-id="' + optionId + '"]').trigger('click'); + var elem = this.element.find('.' + this.options.classes.attributeClass + + '[attribute-code="' + attributeCode + '"] [option-id="' + optionId + '"]'), + parentInput = elem.parent(); + + if (elem.hasClass('selected')) { + return; + } + + if (parentInput.hasClass(this.options.classes.selectClass)) { + parentInput.val(optionId); + parentInput.trigger('change'); + } else { + elem.trigger('click'); + } }, this)); }, diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php index 96fc9a40d53d1..450203486f364 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php @@ -31,7 +31,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic /** * @var string */ - protected $_template = 'rate/form.phtml'; + protected $_template = 'Magento_Tax::rate/form.phtml'; /** * Tax data diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Title.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Title.php index e1e866c06571b..9612b57f8d5d8 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Title.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Title.php @@ -23,7 +23,7 @@ class Title extends \Magento\Framework\View\Element\Template /** * @var string */ - protected $_template = 'rate/title.phtml'; + protected $_template = 'Magento_Tax::rate/title.phtml'; /** * @var \Magento\Store\Model\StoreFactory diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Add.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Add.php index 9cf96bc21e962..16d828542c5b9 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Add.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Add.php @@ -20,7 +20,7 @@ class Add extends \Magento\Backend\Block\Template implements \Magento\Backend\Bl /** * @var string */ - protected $_template = 'toolbar/rate/add.phtml'; + protected $_template = 'Magento_Tax::toolbar/rate/add.phtml'; /** * @var \Magento\Backend\Block\Widget\Button\ButtonList diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php index 19c5fab72ac4b..4eaaa3be8a8f2 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php @@ -16,7 +16,7 @@ class Save extends \Magento\Backend\Block\Template implements \Magento\Backend\B /** * @var string */ - protected $_template = 'toolbar/rate/save.phtml'; + protected $_template = 'Magento_Tax::toolbar/rate/save.phtml'; /** * @var \Magento\Backend\Block\Widget\Button\ButtonList diff --git a/app/code/Magento/Tax/Block/Checkout/Grandtotal.php b/app/code/Magento/Tax/Block/Checkout/Grandtotal.php index 68de4cb24a487..77af1ad99ea2c 100644 --- a/app/code/Magento/Tax/Block/Checkout/Grandtotal.php +++ b/app/code/Magento/Tax/Block/Checkout/Grandtotal.php @@ -15,7 +15,7 @@ class Grandtotal extends \Magento\Checkout\Block\Total\DefaultTotal * * @var string */ - protected $_template = 'checkout/grandtotal.phtml'; + protected $_template = 'Magento_Tax::checkout/grandtotal.phtml'; /** * @var \Magento\Tax\Model\Config diff --git a/app/code/Magento/Tax/Block/Checkout/Shipping.php b/app/code/Magento/Tax/Block/Checkout/Shipping.php index e9098035053be..299c586fd224c 100644 --- a/app/code/Magento/Tax/Block/Checkout/Shipping.php +++ b/app/code/Magento/Tax/Block/Checkout/Shipping.php @@ -15,7 +15,7 @@ class Shipping extends \Magento\Checkout\Block\Total\DefaultTotal * * @var string */ - protected $_template = 'checkout/shipping.phtml'; + protected $_template = 'Magento_Tax::checkout/shipping.phtml'; /** * @var \Magento\Tax\Model\Config diff --git a/app/code/Magento/Tax/Block/Checkout/Subtotal.php b/app/code/Magento/Tax/Block/Checkout/Subtotal.php index 7a9059df08bab..22da07954159d 100644 --- a/app/code/Magento/Tax/Block/Checkout/Subtotal.php +++ b/app/code/Magento/Tax/Block/Checkout/Subtotal.php @@ -15,7 +15,7 @@ class Subtotal extends \Magento\Checkout\Block\Total\DefaultTotal * * @var string */ - protected $_template = 'checkout/subtotal.phtml'; + protected $_template = 'Magento_Tax::checkout/subtotal.phtml'; /** * @var \Magento\Tax\Model\Config diff --git a/app/code/Magento/Tax/Block/Checkout/Tax.php b/app/code/Magento/Tax/Block/Checkout/Tax.php index f741e64019de1..0a86c0312ab1c 100644 --- a/app/code/Magento/Tax/Block/Checkout/Tax.php +++ b/app/code/Magento/Tax/Block/Checkout/Tax.php @@ -14,5 +14,5 @@ class Tax extends \Magento\Checkout\Block\Total\DefaultTotal /** * @var string */ - protected $_template = 'checkout/tax.phtml'; + protected $_template = 'Magento_Tax::checkout/tax.phtml'; } diff --git a/app/code/Magento/Tax/Test/Unit/Model/TaxClass/Type/CustomerTest.php b/app/code/Magento/Tax/Test/Unit/Model/TaxClass/Type/CustomerTest.php index fc27e68c8e55b..707b999c5e467 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/TaxClass/Type/CustomerTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/TaxClass/Type/CustomerTest.php @@ -26,9 +26,9 @@ public function testIsAssignedToObjects() $filterBuilder->expects($this->once())->method('setField')->with( \Magento\Customer\Api\Data\GroupInterface::TAX_CLASS_ID - )->willReturnself(); - $filterBuilder->expects($this->once())->method('setValue')->willReturnself(); - $filterBuilder->expects($this->once())->method('create')->willReturnself(); + )->willReturnSelf(); + $filterBuilder->expects($this->once())->method('setValue')->willReturnSelf(); + $filterBuilder->expects($this->once())->method('create')->willReturnSelf(); $filterGroupBuilder = $this->createMock(\Magento\Framework\Api\Search\FilterGroupBuilder::class); $searchCriteriaBuilder = $this->getMockBuilder(\Magento\Framework\Api\SearchCriteriaBuilder::class) diff --git a/app/code/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExport.php b/app/code/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExport.php index a42877b3ecf8a..ab64567f4fe28 100644 --- a/app/code/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExport.php +++ b/app/code/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExport.php @@ -14,7 +14,7 @@ class ImportExport extends \Magento\Backend\Block\Widget /** * @var string */ - protected $_template = 'importExport.phtml'; + protected $_template = 'Magento_TaxImportExport::importExport.phtml'; /** * @param \Magento\Backend\Block\Template\Context $context diff --git a/app/code/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExportHeader.php b/app/code/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExportHeader.php index 8897e9b2083e9..e223adc3adb1a 100644 --- a/app/code/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExportHeader.php +++ b/app/code/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExportHeader.php @@ -16,5 +16,5 @@ class ImportExportHeader extends \Magento\Backend\Block\Widget * * @var string */ - protected $_template = 'importExportHeader.phtml'; + protected $_template = 'Magento_TaxImportExport::importExportHeader.phtml'; } diff --git a/app/code/Magento/Theme/Block/Adminhtml/Wysiwyg/Files/Content/Uploader.php b/app/code/Magento/Theme/Block/Adminhtml/Wysiwyg/Files/Content/Uploader.php index 8e7f4c9cc680c..e99500dbd0694 100644 --- a/app/code/Magento/Theme/Block/Adminhtml/Wysiwyg/Files/Content/Uploader.php +++ b/app/code/Magento/Theme/Block/Adminhtml/Wysiwyg/Files/Content/Uploader.php @@ -19,7 +19,7 @@ class Uploader extends \Magento\Backend\Block\Media\Uploader * * @var string */ - protected $_template = 'browser/content/uploader.phtml'; + protected $_template = 'Magento_Theme::browser/content/uploader.phtml'; /** * @var \Magento\Theme\Helper\Storage diff --git a/app/code/Magento/Theme/Block/Html/Breadcrumbs.php b/app/code/Magento/Theme/Block/Html/Breadcrumbs.php index c1f8ea620ef41..cff87fc8726bd 100644 --- a/app/code/Magento/Theme/Block/Html/Breadcrumbs.php +++ b/app/code/Magento/Theme/Block/Html/Breadcrumbs.php @@ -21,7 +21,7 @@ class Breadcrumbs extends \Magento\Framework\View\Element\Template * * @var string */ - protected $_template = 'html/breadcrumbs.phtml'; + protected $_template = 'Magento_Theme::html/breadcrumbs.phtml'; /** * List of available breadcrumb properties diff --git a/app/code/Magento/Theme/Block/Html/Header.php b/app/code/Magento/Theme/Block/Html/Header.php index f597b4034da92..2663a4da15011 100644 --- a/app/code/Magento/Theme/Block/Html/Header.php +++ b/app/code/Magento/Theme/Block/Html/Header.php @@ -19,7 +19,7 @@ class Header extends \Magento\Framework\View\Element\Template * * @var string */ - protected $_template = 'html/header.phtml'; + protected $_template = 'Magento_Theme::html/header.phtml'; /** * Retrieve welcome text diff --git a/app/code/Magento/Theme/Block/Html/Header/Logo.php b/app/code/Magento/Theme/Block/Html/Header/Logo.php index 5b0c2eaf04c45..0a0e71f44ba32 100644 --- a/app/code/Magento/Theme/Block/Html/Header/Logo.php +++ b/app/code/Magento/Theme/Block/Html/Header/Logo.php @@ -19,7 +19,7 @@ class Logo extends \Magento\Framework\View\Element\Template * * @var string */ - protected $_template = 'html/header/logo.phtml'; + protected $_template = 'Magento_Theme::html/header/logo.phtml'; /** * @var \Magento\MediaStorage\Helper\File\Storage\Database diff --git a/app/code/Magento/Theme/Controller/Adminhtml/Design/Config/Save.php b/app/code/Magento/Theme/Controller/Adminhtml/Design/Config/Save.php index 657fbb25795d9..4f7a1cd47af58 100644 --- a/app/code/Magento/Theme/Controller/Adminhtml/Design/Config/Save.php +++ b/app/code/Magento/Theme/Controller/Adminhtml/Design/Config/Save.php @@ -7,6 +7,7 @@ use Magento\Backend\App\Action; use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\Exception\NotFoundException; use Magento\Theme\Model\DesignConfigRepository; use Magento\Backend\App\Action\Context; use Magento\Framework\Exception\LocalizedException; @@ -62,9 +63,15 @@ protected function _isAllowed() /** * @return \Magento\Framework\Controller\Result\Redirect + * + * @throws NotFoundException */ public function execute() { + if (!$this->getRequest()->isPost()) { + throw new NotFoundException(__('Page not found.')); + } + $resultRedirect = $this->resultRedirectFactory->create(); $scope = $this->getRequest()->getParam('scope'); $scopeId = (int)$this->getRequest()->getParam('scope_id'); @@ -73,7 +80,7 @@ public function execute() try { $designConfigData = $this->configFactory->create($scope, $scopeId, $data); $this->designConfigRepository->save($designConfigData); - $this->messageManager->addSuccess(__('You saved the configuration.')); + $this->messageManager->addSuccessMessage(__('You saved the configuration.')); $this->dataPersistor->clear('theme_design_config'); @@ -86,10 +93,10 @@ public function execute() } catch (LocalizedException $e) { $messages = explode("\n", $e->getMessage()); foreach ($messages as $message) { - $this->messageManager->addError(__('%1', $message)); + $this->messageManager->addErrorMessage(__('%1', $message)); } } catch (\Exception $e) { - $this->messageManager->addException( + $this->messageManager->addExceptionMessage( $e, __('Something went wrong while saving this configuration:') . ' ' . $e->getMessage() ); diff --git a/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/Design/Config/SaveTest.php b/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/Design/Config/SaveTest.php index f90e24678d9e0..a193604a0d6da 100644 --- a/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/Design/Config/SaveTest.php +++ b/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/Design/Config/SaveTest.php @@ -62,15 +62,13 @@ public function setUp() '', false ); - $this->request = $this->getMockForAbstractClass( - \Magento\Framework\App\RequestInterface::class, - [], - '', - false, - false, - true, - ['getFiles', 'getParam', 'getParams'] - ); + $this->request = $this->getMockBuilder(\Magento\Framework\App\Request\Http::class) + ->disableOriginalConstructor()->getMock(); + + $this->request->expects($this->atLeastOnce()) + ->method('isPost') + ->willReturn(true); + $this->context = $objectManager->getObject( \Magento\Backend\App\Action\Context::class, [ @@ -138,7 +136,7 @@ public function testSave() ->method('save') ->with($this->designConfig); $this->messageManager->expects($this->once()) - ->method('addSuccess') + ->method('addSuccessMessage') ->with(__('You saved the configuration.')); $this->dataPersistor->expects($this->once()) ->method('clear') @@ -194,7 +192,7 @@ public function testSaveWithLocalizedException() ->with($this->designConfig) ->willThrowException(new \Magento\Framework\Exception\LocalizedException(__('Exception message'))); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('Exception message')->render()); $this->dataPersistor->expects($this->once()) @@ -249,7 +247,7 @@ public function testSaveWithException() ->with($this->designConfig) ->willThrowException($exception); $this->messageManager->expects($this->once()) - ->method('addException') + ->method('addExceptionMessage') ->with($exception, 'Something went wrong while saving this configuration: Exception message'); $this->dataPersistor->expects($this->once()) diff --git a/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/UploadJsTest.php b/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/UploadJsTest.php index d5919db5edcba..bbcaa87acb9c3 100644 --- a/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/UploadJsTest.php +++ b/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/UploadJsTest.php @@ -63,22 +63,22 @@ public function testExecuteWithoutTheme() ->expects($this->at(0)) ->method('get') ->with(\Magento\Theme\Model\Uploader\Service::class) - ->WillReturn($this->serviceModel); + ->willReturn($this->serviceModel); $this->_objectManagerMock ->expects($this->at(1)) ->method('get') ->with(\Magento\Framework\View\Design\Theme\FlyweightFactory::class) - ->WillReturn($this->themeFactory); + ->willReturn($this->themeFactory); $this->_objectManagerMock ->expects($this->at(2)) ->method('get') ->with(\Magento\Framework\View\Design\Theme\Customization\File\Js::class) - ->WillReturn($this->customizationJs); + ->willReturn($this->customizationJs); $this->_objectManagerMock ->expects($this->at(3)) ->method('get') ->with(\Magento\Framework\Json\Helper\Data::class) - ->WillReturn($this->jsonHelper); + ->willReturn($this->jsonHelper); $this->themeFactory->expects($this->once()) ->method('create') @@ -107,21 +107,21 @@ public function testExecuteWithException() $this->_objectManagerMock->expects($this->at(0)) ->method('get') ->with(\Magento\Theme\Model\Uploader\Service::class) - ->WillReturn($this->serviceModel); + ->willReturn($this->serviceModel); $this->_objectManagerMock->expects($this->at(1)) ->method('get') ->with(\Magento\Framework\View\Design\Theme\FlyweightFactory::class) - ->WillReturn($this->themeFactory); + ->willReturn($this->themeFactory); $this->_objectManagerMock ->expects($this->at(2)) ->method('get') ->with(\Magento\Framework\View\Design\Theme\Customization\File\Js::class) - ->WillReturn($this->customizationJs); + ->willReturn($this->customizationJs); $this->_objectManagerMock ->expects($this->at(4)) ->method('get') ->with(\Magento\Framework\Json\Helper\Data::class) - ->WillReturn($this->jsonHelper); + ->willReturn($this->jsonHelper); $this->themeFactory->expects($this->once()) ->method('create') @@ -172,19 +172,19 @@ public function testExecute() $this->_objectManagerMock->expects($this->at(0)) ->method('get') ->with(\Magento\Theme\Model\Uploader\Service::class) - ->WillReturn($this->serviceModel); + ->willReturn($this->serviceModel); $this->_objectManagerMock->expects($this->at(1)) ->method('get') ->with(\Magento\Framework\View\Design\Theme\FlyweightFactory::class) - ->WillReturn($this->themeFactory); + ->willReturn($this->themeFactory); $this->_objectManagerMock->expects($this->at(2)) ->method('get') ->with(\Magento\Framework\View\Design\Theme\Customization\File\Js::class) - ->WillReturn($this->customizationJs); + ->willReturn($this->customizationJs); $this->_objectManagerMock->expects($this->at(4)) ->method('get') ->with(\Magento\Framework\Json\Helper\Data::class) - ->WillReturn($this->jsonHelper); + ->willReturn($this->jsonHelper); $this->themeFactory->expects($this->once()) ->method('create') diff --git a/app/code/Magento/Ui/Model/Export/ConvertToCsv.php b/app/code/Magento/Ui/Model/Export/ConvertToCsv.php index 40b10749db21e..eb811bfae788f 100644 --- a/app/code/Magento/Ui/Model/Export/ConvertToCsv.php +++ b/app/code/Magento/Ui/Model/Export/ConvertToCsv.php @@ -9,7 +9,6 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Ui\Component\MassAction\Filter; /** @@ -18,7 +17,7 @@ class ConvertToCsv { /** - * @var WriteInterface + * @var DirectoryList */ protected $directory; diff --git a/app/code/Magento/Ui/Model/Export/ConvertToXml.php b/app/code/Magento/Ui/Model/Export/ConvertToXml.php index 5f6e45a948f24..19eb651113fcf 100644 --- a/app/code/Magento/Ui/Model/Export/ConvertToXml.php +++ b/app/code/Magento/Ui/Model/Export/ConvertToXml.php @@ -13,7 +13,6 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Ui\Component\MassAction\Filter; /** @@ -22,7 +21,7 @@ class ConvertToXml { /** - * @var WriteInterface + * @var DirectoryList */ protected $directory; diff --git a/app/code/Magento/Ui/Model/Export/MetadataProvider.php b/app/code/Magento/Ui/Model/Export/MetadataProvider.php index 499cd06e505f6..54d856e8a6104 100644 --- a/app/code/Magento/Ui/Model/Export/MetadataProvider.php +++ b/app/code/Magento/Ui/Model/Export/MetadataProvider.php @@ -118,6 +118,13 @@ public function getHeaders(UiComponentInterface $component) foreach ($this->getColumns($component) as $column) { $row[] = $column->getData('config/label'); } + + array_walk($row, function (&$header) { + if (mb_strpos($header, 'ID') === 0) { + $header = '"' . $header . '"'; + } + }); + return $row; } diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml index d05a7898872a0..9239f296aafad 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminDataGridPaginationActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Test/etc/actionGroupSchema.xsd"> <actionGroup name="adminDataGridSelectPerPage"> <arguments> - <argument name="perPage"/> + <argument name="perPage" type="string"/> </arguments> <click selector="{{AdminDataGridPaginationSection.perPageDropdown}}" stepKey="clickPerPageDropdown"/> <click selector="{{AdminDataGridPaginationSection.perPageOption(perPage)}}" stepKey="selectCustomPerPage"/> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridFilterSearchResultsActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridFilterSearchResultsActionGroup.xml index 41de4141b4c58..023e20b7025c1 100644 --- a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridFilterSearchResultsActionGroup.xml +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridFilterSearchResultsActionGroup.xml @@ -11,10 +11,10 @@ <actionGroup name="AdminGridFilterSearchResultsByInput"> <arguments> <argument name="selector"/> - <argument name="value"/> + <argument name="value" type="string"/> </arguments> - <conditionalClick selector="{{AdminGridFilterControls.clearAll}}" dependentSelector=".admin__data-grid-header[data-bind='afterRender: \$data.setToolbarNode'] .admin__data-grid-filters-current._show" visible="true" stepKey="clearTheFiltersIfPresent"/> + <conditionalClick selector="{{AdminGridFilterControls.clearAll}}" dependentSelector="(//*[contains(@class, 'admin__data-grid-header')][contains(@data-bind, 'afterRender: \$data.setToolbarNode')]//*[contains(@class, 'admin__data-grid-filters-current')][contains(@class, '_show')])[1]" visible="true" stepKey="clearTheFiltersIfPresent"/> <waitForPageLoad stepKey="waitForPageLoad" time="5"/> <click selector="{{AdminGridFilterControls.filters}}" stepKey="clickOnFilters1"/> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml index 5e4fc5298bba8..ff4097aa76265 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> <section name="AdminDataGridPaginationSection"> <element name="perPageDropdown" type="select" selector=".admin__data-grid-pager-wrap .selectmenu"/> - <element name="perPageOption" type="button" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//button[text()='{{label}}']" parameterized="true"/> + <element name="perPageOption" type="button" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//button[text()='{{var1}}']" parameterized="true"/> <element name="perPageInput" type="input" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//div[@class='selectmenu-item-edit']//input"/> <element name="perPageApplyInput" type="button" selector="//div[@class='admin__data-grid-pager-wrap']//div[@class='selectmenu-items _active']//li//div[@class='selectmenu-item-edit']//button"/> <element name="nextPage" type="button" selector="div.admin__data-grid-pager > button.action-next" timeout="30"/> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml index 2164af26e5ac4..ea0f7e64a8448 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml @@ -12,6 +12,7 @@ <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)"/> <element name="columnHeader" type="button" selector="//div[@data-role='grid-wrapper']//table[contains(@class, 'data-grid')]/thead/tr/th[contains(@class, 'data-grid-th')]/span[text() = '{{label}}']" parameterized="true" timeout="30"/> <element name="column" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{col}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> + <element name="rowCheckbox" type="checkbox" selector="table.data-grid tbody > tr:nth-of-type({{row}}) td.data-grid-checkbox-cell input" parameterized="true"/> <element name="row" type="text" selector="table.data-grid tbody > tr:nth-of-type({{row}})" parameterized="true"/> <element name="rows" type="text" selector="table.data-grid tbody > tr.data-row"/> <!--Specific cell e.g. {{Section.gridCell('1', 'Name')}}--> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection.xml index ac1e461e9e16b..2bf65a682d21c 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection.xml @@ -27,7 +27,7 @@ <element name="filters" type="button" selector=".admin__data-grid-header[data-bind='afterRender: \$data.setToolbarNode'] button[data-action='grid-filter-expand']" timeout="5"/> <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> <element name="cancel" type="button" selector="button[data-action='grid-filter-cancel']" timeout="30"/> - <element name="clearAll" type="button" selector=".admin__data-grid-header[data-bind='afterRender: \$data.setToolbarNode'] button[data-action='grid-filter-reset']" timeout="5"/> + <element name="clearAll" type="button" selector="(//*[contains(@class, 'admin__data-grid-header')][contains(@data-bind, 'afterRender: \$data.setToolbarNode')]//button[contains(@data-action, 'reset')])[1]" timeout="5"/> </section> <section name="AdminGridDefaultViewControls"> <element name="defaultView" type="button" selector=".admin__data-grid-header[data-bind='afterRender: \$data.setToolbarNode'] .admin__data-grid-action-bookmarks" timeout="5"/> @@ -54,7 +54,8 @@ <!-- TODO: Pagination controls --> <section name="AdminGridHeaders"> <element name="title" type="text" selector=".page-title-wrapper h1"/> - <element name="headerByName" type="text" selector="//span[@class='data-grid-cell-content' and contains(text(), '{{var1}}')]/parent::*" parameterized="true"/> + <element name="headerByName" type="text" selector="//div[@data-role='grid-wrapper']//span[@class='data-grid-cell-content' and contains(text(), '{{var1}}')]/parent::*" parameterized="true"/> + <element name="columnsNames" type="text" selector="[data-role='grid-wrapper'] .data-grid-th > span"/> </section> <section name="AdminGridRow"> <element name="rowOne" type="text" selector="tr[data-repeat-index='0']"/> @@ -65,4 +66,16 @@ <element name="checkboxByValue" type="checkbox" selector="//input[ancestor::tr[contains(., '{{var1}}')]]" parameterized="true"/> <element name="checkboxByIndex" type="checkbox" selector=".data-row[data-repeat-index='{{var1}}'] .admin__control-checkbox" parameterized="true"/> </section> + <section name="AdminGridSelectRows"> + <element name="multicheckDropdown" type="button" selector="div[data-role='grid-wrapper'] th.data-grid-multicheck-cell button.action-multicheck-toggle"/> + <element name="multicheckOption" type="button" selector="//div[@data-role='grid-wrapper']//th[contains(@class, data-grid-multicheck-cell)]//li//span[text() = '{{label}}']" parameterized="true"/> + <element name="bulkActionDropdown" type="button" selector="div.admin__data-grid-header-row.row div.action-select-wrap button.action-select"/> + <element name="bulkActionOption" type="button" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-select-wrap')]//ul/li/span[text() = '{{label}}']" parameterized="true"/> + </section> + <section name="AdminGridConfirmActionSection"> + <element name="title" type="text" selector=".modal-popup.confirm h1.modal-title"/> + <element name="message" type="text" selector=".modal-popup.confirm div.modal-content"/> + <element name="cancel" type="button" selector=".modal-popup.confirm button.action-dismiss"/> + <element name="ok" type="button" selector=".modal-popup.confirm button.action-accept" timeout="60"/> + </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml new file mode 100644 index 0000000000000..d3e94eb24dfd2 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminMessagesSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="../../../../../../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/SectionObject.xsd"> + <section name="AdminMessagesSection"> + <element name="successMessage" type="text" selector=".message-success"/> + <element name="errorMessage" type="text" selector=".message.message-error.error"/> + </section> +</sections> diff --git a/app/code/Magento/Ui/Test/Unit/Model/Export/MetadataProviderTest.php b/app/code/Magento/Ui/Test/Unit/Model/Export/MetadataProviderTest.php index dd6fd58d355e6..aae4fafbdac3f 100644 --- a/app/code/Magento/Ui/Test/Unit/Model/Export/MetadataProviderTest.php +++ b/app/code/Magento/Ui/Test/Unit/Model/Export/MetadataProviderTest.php @@ -40,18 +40,37 @@ protected function setUp() ); } - public function testGetHeaders() + /** + * @param array $columnLabels + * @param array $expected + * @return void + * @dataProvider getColumnsDataProvider + */ + public function testGetHeaders(array $columnLabels, array $expected): void { $componentName = 'component_name'; $columnName = 'column_name'; - $columnLabel = 'column_label'; - - $component = $this->prepareColumns($componentName, $columnName, $columnLabel); + $component = $this->prepareColumns($componentName, $columnName, $columnLabels[0]); $result = $this->model->getHeaders($component); $this->assertTrue(is_array($result)); $this->assertCount(1, $result); - $this->assertEquals($columnLabel, $result[0]); + $this->assertEquals($expected, $result); + } + + /** + * @return array + */ + public function getColumnsDataProvider(): array + { + return [ + [['ID'],['"ID"']], + [['Name'],['Name']], + [['Id'],['Id']], + [['id'],['id']], + [['IDTEST'],['"IDTEST"']], + [['ID TEST'],['"ID TEST"']], + ]; } public function testGetFields() diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/region.js b/app/code/Magento/Ui/view/base/web/js/form/element/region.js index 0edb4c1966b54..fec69bf8558b5 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/region.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/region.js @@ -57,6 +57,7 @@ define([ registry.get(this.customName, function (input) { isRegionRequired = !!option['is_region_required']; input.validation['required-entry'] = isRegionRequired; + input.validation['validate-not-number-first'] = true; input.required(isRegionRequired); }); } diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js index 41e2703f4ed1e..c63b4588d2d16 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js @@ -755,6 +755,12 @@ define([ }, $.mage.__('Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.')//eslint-disable-line max-len ], + 'validate-not-number-first': [ + function (value) { + return utils.isEmptyNoTrim(value) || /^[^0-9-\.].*$/.test(value.trim()); + }, + $.mage.__('First character must be letter.') + ], 'validate-date': [ function (value, params, additionalParams) { var test = moment(value, additionalParams.dateFormat); diff --git a/app/code/Magento/Ui/view/base/web/js/modal/modal.js b/app/code/Magento/Ui/view/base/web/js/modal/modal.js index 6c9b4b89bec7a..147a18c08c54c 100644 --- a/app/code/Magento/Ui/view/base/web/js/modal/modal.js +++ b/app/code/Magento/Ui/view/base/web/js/modal/modal.js @@ -104,11 +104,12 @@ define([ /** * Escape key press handler, * close modal window + * @param {Object} event - event */ - escapeKey: function () { + escapeKey: function (event) { if (this.options.isOpen && this.modal.find(document.activeElement).length || this.options.isOpen && this.modal[0] === document.activeElement) { - this.closeModal(); + this.closeModal(event); } } } diff --git a/app/code/Magento/UrlRewrite/Block/Catalog/Category/Tree.php b/app/code/Magento/UrlRewrite/Block/Catalog/Category/Tree.php index 64a775b01593b..e34d4773c271b 100644 --- a/app/code/Magento/UrlRewrite/Block/Catalog/Category/Tree.php +++ b/app/code/Magento/UrlRewrite/Block/Catalog/Category/Tree.php @@ -27,7 +27,7 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory /** * @var string */ - protected $_template = 'categories.phtml'; + protected $_template = 'Magento_UrlRewrite::categories.phtml'; /** * Adminhtml data diff --git a/app/code/Magento/UrlRewrite/Block/Selector.php b/app/code/Magento/UrlRewrite/Block/Selector.php index 0a28ba215de5a..75266fd2f977c 100644 --- a/app/code/Magento/UrlRewrite/Block/Selector.php +++ b/app/code/Magento/UrlRewrite/Block/Selector.php @@ -18,7 +18,7 @@ class Selector extends \Magento\Backend\Block\Template /** * @var string */ - protected $_template = 'selector.phtml'; + protected $_template = 'Magento_UrlRewrite::selector.phtml'; /** * Set block template and get available modes diff --git a/app/code/Magento/User/Block/Role/Tab/Edit.php b/app/code/Magento/User/Block/Role/Tab/Edit.php index 45d725c61bd52..5fe6a1b2a2e88 100644 --- a/app/code/Magento/User/Block/Role/Tab/Edit.php +++ b/app/code/Magento/User/Block/Role/Tab/Edit.php @@ -19,7 +19,7 @@ class Edit extends \Magento\Backend\Block\Widget\Form implements \Magento\Backen /** * @var string */ - protected $_template = 'role/edit.phtml'; + protected $_template = 'Magento_User::role/edit.phtml'; /** * Root ACL Resource diff --git a/app/code/Magento/User/Test/Unit/Model/ResourceModel/UserTest.php b/app/code/Magento/User/Test/Unit/Model/ResourceModel/UserTest.php index 16cde3cfe2c06..1cc0cd54c60eb 100644 --- a/app/code/Magento/User/Test/Unit/Model/ResourceModel/UserTest.php +++ b/app/code/Magento/User/Test/Unit/Model/ResourceModel/UserTest.php @@ -330,7 +330,7 @@ public function testDeleteFromRole() $roleId = 44; $methodUserMock->expects($this->once())->method('getUserId')->willReturn($uid); $this->resourceMock->expects($this->atLeastOnce())->method('getConnection')->willReturn($this->dbAdapterMock); - $methodUserMock->expects($this->atleastOnce())->method('getRoleId')->willReturn($roleId); + $methodUserMock->expects($this->atLeastOnce())->method('getRoleId')->willReturn($roleId); $this->dbAdapterMock->expects($this->once())->method('delete'); $this->assertInstanceOf( diff --git a/app/code/Magento/Weee/Block/Renderer/Weee/Tax.php b/app/code/Magento/Weee/Block/Renderer/Weee/Tax.php index ab6710fddcac2..94a6faf72aa96 100644 --- a/app/code/Magento/Weee/Block/Renderer/Weee/Tax.php +++ b/app/code/Magento/Weee/Block/Renderer/Weee/Tax.php @@ -32,7 +32,7 @@ class Tax extends \Magento\Backend\Block\Widget implements /** * @var string */ - protected $_template = 'renderer/tax.phtml'; + protected $_template = 'Magento_Weee::renderer/tax.phtml'; /** * Core registry diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php index 49345f29afd53..c48bf9e7e4c7a 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php @@ -27,7 +27,7 @@ class Layout extends Template implements RendererInterface /** * @var string */ - protected $_template = 'instance/edit/layout.phtml'; + protected $_template = 'Magento_Widget::instance/edit/layout.phtml'; /** * @var \Magento\Catalog\Model\Product\Type diff --git a/app/code/Magento/Wishlist/Block/Rss/EmailLink.php b/app/code/Magento/Wishlist/Block/Rss/EmailLink.php index 4a5f116cd8293..907dfd90e752e 100644 --- a/app/code/Magento/Wishlist/Block/Rss/EmailLink.php +++ b/app/code/Magento/Wishlist/Block/Rss/EmailLink.php @@ -21,7 +21,7 @@ class EmailLink extends Link /** * @var string */ - protected $_template = 'rss/email.phtml'; + protected $_template = 'Magento_Wishlist::rss/email.phtml'; /** * @return array diff --git a/app/code/Magento/Wishlist/Block/Share/Email/Items.php b/app/code/Magento/Wishlist/Block/Share/Email/Items.php index bc84f6a43df3c..d4e6587fd6519 100644 --- a/app/code/Magento/Wishlist/Block/Share/Email/Items.php +++ b/app/code/Magento/Wishlist/Block/Share/Email/Items.php @@ -20,7 +20,7 @@ class Items extends \Magento\Wishlist\Block\AbstractBlock /** * @var string */ - protected $_template = 'email/items.phtml'; + protected $_template = 'Magento_Wishlist::email/items.phtml'; /** * Retrieve Product View URL diff --git a/app/code/Magento/Wishlist/Model/Item.php b/app/code/Magento/Wishlist/Model/Item.php index b0e7c78cae5f4..41e83c7179e10 100644 --- a/app/code/Magento/Wishlist/Model/Item.php +++ b/app/code/Magento/Wishlist/Model/Item.php @@ -473,7 +473,7 @@ public function getProductUrl() public function getBuyRequest() { $option = $this->getOptionByCode('info_buyRequest'); - $initialData = $option ? $this->serializer->unserialize($option->getValue()) : null; + $initialData = $option ? $this->serializer->unserialize($option->getValue()) : []; if ($initialData instanceof \Magento\Framework\DataObject) { $initialData = $initialData->getData(); diff --git a/app/design/frontend/Magento/blank/web/css/source/components/_modals_extend.less b/app/design/frontend/Magento/blank/web/css/source/components/_modals_extend.less index d76630b5cea47..5cdb1444094e9 100644 --- a/app/design/frontend/Magento/blank/web/css/source/components/_modals_extend.less +++ b/app/design/frontend/Magento/blank/web/css/source/components/_modals_extend.less @@ -63,6 +63,8 @@ } .modal-popup { + pointer-events: none; + .modal-title { .lib-css(border-bottom, @modal-title__border); .lib-css(font-weight, @font-weight__light); diff --git a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less index e90d312aa7801..ae68fc81acd6e 100644 --- a/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less +++ b/app/design/frontend/Magento/luma/web/css/source/components/_modals_extend.less @@ -63,6 +63,8 @@ } .modal-popup { + pointer-events: none; + .modal-title { .lib-css(border-bottom, @modal-title__border); .lib-css(font-weight, @font-weight__light); diff --git a/dev/tests/acceptance/tests/_data/lorem_ipsum.docx b/dev/tests/acceptance/tests/_data/lorem_ipsum.docx new file mode 100644 index 0000000000000..488f64e86b6ff Binary files /dev/null and b/dev/tests/acceptance/tests/_data/lorem_ipsum.docx differ diff --git a/dev/tests/acceptance/tests/_data/lorem_ipsum.txt b/dev/tests/acceptance/tests/_data/lorem_ipsum.txt new file mode 100644 index 0000000000000..64cb8d023361a --- /dev/null +++ b/dev/tests/acceptance/tests/_data/lorem_ipsum.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc venenatis cursus eros, eu congue risus eleifend ut. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean aliquet mi diam, at consequat ex imperdiet eu. Nullam vulputate sollicitudin libero, tristique ullamcorper ante pretium eget. Nunc vehicula, risus ut hendrerit ornare, tortor est mattis urna, vitae egestas arcu tellus quis est. Sed mollis est sem. Aenean rhoncus ultricies sapien, id tempus elit lobortis ac. Pellentesque condimentum gravida purus a pretium. Nulla sed sapien mattis, auctor lacus quis, volutpat metus. Nunc mattis diam elit, viverra tincidunt nisl faucibus eu. Duis ac nisl tellus. Aenean eros lectus, malesuada in ex non, pharetra aliquam odio. Aenean ultricies pharetra mauris, ac rutrum quam posuere at. Phasellus et tempor turpis, ut sodales turpis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. + +Sed fringilla orci at elit gravida, posuere varius elit eleifend. Nam libero dui, rutrum ac massa et, dictum egestas massa. Fusce rutrum, neque vitae vestibulum mattis, magna orci dictum turpis, ut laoreet eros urna lacinia ipsum. Donec eget ultrices eros. Duis sollicitudin ante est. Maecenas semper pellentesque scelerisque. Vestibulum eu venenatis tellus. Etiam nec massa sem. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam blandit molestie justo, non euismod dolor aliquet ac. Duis consectetur enim in arcu suscipit, in tempus nisi commodo. + +Donec sed venenatis nunc. Proin velit leo, porta eget erat elementum, dapibus posuere odio. Nam varius lectus eu cursus tristique. Ut tempus libero vehicula, iaculis augue sit amet, vestibulum justo. Vivamus porta diam vitae malesuada vestibulum. Donec mi dolor, semper at rutrum eget, vehicula non orci. Nunc dolor urna, laoreet et egestas vitae, sodales quis ipsum. Vivamus aliquet viverra enim cursus tincidunt. Pellentesque vel laoreet mi. Aenean ut rhoncus orci. Donec a purus venenatis, tempor dolor in, facilisis ex. Sed nec metus convallis, viverra nisl nec, luctus arcu. Integer blandit arcu a est posuere pharetra. + +Aliquam ultricies lectus ac mauris luctus, a viverra neque rhoncus. Vestibulum id velit eu nisl efficitur lobortis. Sed id metus at ipsum imperdiet porta. Quisque in quam in turpis fermentum condimentum. Phasellus sagittis risus eu tempus scelerisque. Vivamus dapibus sem odio, vitae fermentum sem viverra et. Quisque sit amet cursus neque, vel hendrerit risus. Integer ut diam porta, volutpat risus in, iaculis diam. Suspendisse vulputate non quam et finibus. + +Donec blandit, sem ut posuere dignissim, dolor lorem egestas magna, vel faucibus dui metus eget orci. Nullam ipsum lacus, imperdiet at nisl sed, condimentum dignissim lectus. Nunc orci libero, tincidunt a molestie vel, dapibus at mi. Quisque scelerisque sem quis massa suscipit, sit amet suscipit arcu volutpat. In tincidunt lacus in porttitor mattis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Morbi rutrum gravida orci quis porta. \ No newline at end of file diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls index 3466db5c71f6a..7eb175a88e322 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls @@ -5,7 +5,16 @@ type Query { testItem(id: Int!) : Item @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") } +type Mutation { + testItem(id: Int!) : MutationItem @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") +} + type Item { item_id: Int name: String } + +type MutationItem { + item_id: Int + name: String +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls index dc01d993c3818..b970ad8376349 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls @@ -4,3 +4,7 @@ type Item { integer_list: [Int] @resolver(class: "Magento\\TestModuleGraphQlQueryExtension\\Model\\Resolver\\IntegerList") } + +type MutationItem { + integer_list: [Int] @resolver(class: "Magento\\TestModuleGraphQlQueryExtension\\Model\\Resolver\\IntegerList") +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php new file mode 100644 index 0000000000000..d9a46a4451212 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryProductsVariantsTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test of getting child products info of configurable product on category request + */ +class CategoryProductsVariantsTest extends GraphQlAbstract +{ + /** + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testGetSimpleProductsFromCategory() + { + + $query + = <<<QUERY +{ + category(id: 2) { + id + name + products { + items { + sku + ... on ConfigurableProduct { + variants { + product { + sku + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product = $productRepository->get('simple_10', false, null, true); + + $this->assertArrayHasKey('variants', $response['category']['products']['items'][0]); + $this->assertCount(2, $response['category']['products']['items'][0]['variants']); + $this->assertSimpleProductFields($product, $response['category']['products']['items'][0]['variants'][0]); + } + + /** + * @param ProductInterface $product + * @param array $actualResponse + */ + private function assertSimpleProductFields($product, $actualResponse) + { + $assertionMap = [ + [ + 'response_field' => 'product', 'expected_value' => [ + "sku" => $product->getSku() + ] + ], + ]; + + $this->assertResponseFields($actualResponse, $assertionMap); + } + + /** + * @param array $actualResponse + * @param array $assertionMap + */ + private function assertResponseFields($actualResponse, $assertionMap) + { + foreach ($assertionMap as $key => $assertionData) { + $expectedValue = isset($assertionData['expected_value']) + ? $assertionData['expected_value'] + : $assertionData; + $responseField = isset($assertionData['response_field']) ? $assertionData['response_field'] : $key; + self::assertNotNull( + $expectedValue, + "Value of '{$responseField}' field must not be NULL" + ); + self::assertEquals( + $expectedValue, + $actualResponse[$responseField], + "Value of '{$responseField}' field in response does not match expected value: " + . var_export($expectedValue, true) + ); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index 0133b87e757bd..4f68090657a10 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -63,9 +63,6 @@ public function testCategoriesTree() children { level id - children { - id - } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php new file mode 100644 index 0000000000000..b6e1a61f0357c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\TestModule; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Make sure that it is possible to use GraphQL mutations in Magento + */ +class GraphQlMutationTest extends GraphQlAbstract +{ + public function testMutation() + { + $id = 3; + + $query = <<<MUTATION +mutation { + testItem(id: {$id}) { + item_id, + name, + integer_list + } +} +MUTATION; + + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('testItem', $response); + $testItem = $response['testItem']; + $this->assertArrayHasKey('integer_list', $testItem); + $this->assertEquals([4, 5, 6], $testItem['integer_list']); + } +} diff --git a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CreateOnlineCreditMemoPayflowLinkTest.xml b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CreateOnlineCreditMemoPayflowLinkTest.xml index f3922aa474735..f212f48237ecb 100644 --- a/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CreateOnlineCreditMemoPayflowLinkTest.xml +++ b/dev/tests/functional/tests/app/Magento/Paypal/Test/TestCase/CreateOnlineCreditMemoPayflowLinkTest.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> - <testCase name="Magento\Paypal\Test\TestCase\CreateOnlineCreditMemoPayflowLinkTest" summary="Create online credit memo for order placed with with PayPal Payflow Link"> + <testCase name="Magento\Paypal\Test\TestCase\CreateOnlineCreditMemoPayflowLinkTest" summary="Create online credit memo for order placed with PayPal Payflow Link"> <variation name="CreateOnlineCreditMemoPayflowLinkVariation1" summary="Create Refund for Order Paid with PayPal Payflow Link" ticketId="MAGETWO-13061"> <data name="tag" xsi:type="string">test_type:3rd_party_test, severity:S0</data> <data name="products/0" xsi:type="string">catalogProductSimple::product_100_dollar</data> diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml index 10b9d3e80d0c2..521d7d68ac4a6 100644 --- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml +++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml @@ -96,8 +96,8 @@ <field name="stop_rules_processing" xsi:type="string">Yes</field> </dataset> <dataset name="active_sales_rule_with_complex_conditions"> - <field name="name" xsi:type="string">Cart Price Rule with with complex conditions %isolation%</field> - <field name="description" xsi:type="string">Cart Price Rule with with complex conditions</field> + <field name="name" xsi:type="string">Cart Price Rule with complex conditions %isolation%</field> + <field name="description" xsi:type="string">Cart Price Rule with complex conditions</field> <field name="is_active" xsi:type="string">Yes</field> <field name="website_ids" xsi:type="array"> <item name="0" xsi:type="string">Main Website</item> @@ -129,8 +129,8 @@ <field name="stop_rules_processing" xsi:type="string">Yes</field> <field name="simple_free_shipping" xsi:type="string">For matching items only</field> <field name="store_labels" xsi:type="array"> - <item name="0" xsi:type="string">Cart Price Rule with with complex conditions</item> - <item name="1" xsi:type="string">Cart Price Rule with with complex conditions</item> + <item name="0" xsi:type="string">Cart Price Rule with complex conditions</item> + <item name="1" xsi:type="string">Cart Price Rule with complex conditions</item> </field> </dataset> diff --git a/dev/tests/functional/tests/app/Magento/Search/Test/Fixture/SynonymGroup.xml b/dev/tests/functional/tests/app/Magento/Search/Test/Fixture/SynonymGroup.xml index fc30822ab8bed..2cebaf93ff2a9 100644 --- a/dev/tests/functional/tests/app/Magento/Search/Test/Fixture/SynonymGroup.xml +++ b/dev/tests/functional/tests/app/Magento/Search/Test/Fixture/SynonymGroup.xml @@ -13,7 +13,7 @@ collection="Magento\Search\Model\ResourceModel\Block\Grid\Collection" handler_interface="Magento\Search\Test\Handler\SynonymGroup\SynonymGroupInterface" repository_class="Magento\Search\Test\Repository\SynonymGroup" class="Magento\Search\Test\Fixture\SynonymGroup"> - <field name="group_id" is_required="1 "/> + <field name="group_id" is_required="1"/> <field name="synonyms" is_required="0" /> <field name="scope_id" is_required="0" source="Magento\Search\Test\Fixture\SynonymGroup\ScopeId" /> <field name="mergeOnConflict" is_required="0" /> diff --git a/dev/tests/functional/tests/app/Magento/Weee/Test/TestCase/CreateTaxWithFptTest.xml b/dev/tests/functional/tests/app/Magento/Weee/Test/TestCase/CreateTaxWithFptTest.xml index 7f7741d2f1c13..25acb75ee134f 100644 --- a/dev/tests/functional/tests/app/Magento/Weee/Test/TestCase/CreateTaxWithFptTest.xml +++ b/dev/tests/functional/tests/app/Magento/Weee/Test/TestCase/CreateTaxWithFptTest.xml @@ -97,7 +97,7 @@ </variation> <variation name="CreateTaxWithFptTestVariation5" firstConstraint="Magento\Weee\Test\Constraint\AssertFptApplied" method="test"> <data name="tag" xsi:type="string">to_maintain:yes</data> - <data name="description" xsi:type="string">Check taxed FPT display set to Excluding, Description and Including FPT on product with with custom option catalog price Excluding Tax</data> + <data name="description" xsi:type="string">Check taxed FPT display set to Excluding, Description and Including FPT on product with custom option catalog price Excluding Tax</data> <data name="configData" xsi:type="string">shipping_tax_class_taxable_goods,tax_with_fpt_taxed_cat_excl_disc_on_excl</data> <data name="productData" xsi:type="string">with_custom_option_and_fpt</data> <data name="prices/category_price" xsi:type="string">70.00</data> @@ -117,7 +117,7 @@ <constraint name="Magento\Weee\Test\Constraint\AssertFptApplied" /> </variation> <variation name="CreateTaxWithFptTestVariation6" firstConstraint="Magento\Weee\Test\Constraint\AssertFptApplied" method="test"> - <data name="description" xsi:type="string">Check taxed FPT display set to Including FPT and Description on product with with custom option catalog price Excluding Tax</data> + <data name="description" xsi:type="string">Check taxed FPT display set to Including FPT and Description on product with custom option catalog price Excluding Tax</data> <data name="configData" xsi:type="string">shipping_tax_class_taxable_goods,tax_with_fpt_taxed_cat_excl_disc_on_incl, display_including_tax</data> <data name="productData" xsi:type="string">with_custom_option_and_fpt</data> <data name="prices/category_price" xsi:type="string">86.60</data> @@ -173,7 +173,7 @@ <constraint name="Magento\Weee\Test\Constraint\AssertFptApplied" /> </variation> <variation name="CreateTaxWithFptTestVariation9" firstConstraint="Magento\Weee\Test\Constraint\AssertFptApplied" method="test"> - <data name="description" xsi:type="string">Check taxed FPT display set to Excluding, Description and Including FPT on product with with special price and catalog price Including Tax</data> + <data name="description" xsi:type="string">Check taxed FPT display set to Excluding, Description and Including FPT on product with special price and catalog price Including Tax</data> <data name="configData" xsi:type="string">shipping_tax_class_taxable_goods,tax_with_fpt_taxed_cat_incl_disc_on_excl</data> <data name="productData" xsi:type="string">with_special_price_and_fpt</data> <data name="prices/category_price" xsi:type="string">92.38</data> @@ -193,7 +193,7 @@ <constraint name="Magento\Weee\Test\Constraint\AssertFptApplied" /> </variation> <variation name="CreateTaxWithFptTestVariation10" firstConstraint="Magento\Weee\Test\Constraint\AssertFptApplied" method="test"> - <data name="description" xsi:type="string">Check taxed FPT display set to Including FPT and Description on product with with special price and catalog price Including Tax</data> + <data name="description" xsi:type="string">Check taxed FPT display set to Including FPT and Description on product with special price and catalog price Including Tax</data> <data name="configData" xsi:type="string">shipping_tax_class_taxable_goods,tax_with_fpt_taxed_cat_incl_disc_on_incl</data> <data name="productData" xsi:type="string">with_special_price_and_fpt</data> <data name="prices/category_price" xsi:type="string">101.62</data> @@ -210,7 +210,7 @@ </variation> <variation name="CreateTaxWithFptTestVariation11" firstConstraint="Magento\Weee\Test\Constraint\AssertFptApplied" method="test"> <data name="issue" xsi:type="string">MAGETWO-44968: FPT Final price includes tax on custom option, when display is set to excluding tax</data> - <data name="description" xsi:type="string">Check taxed FPT display set to Excluding, Description and Including FPT on product with with custom option and catalog price Including Tax</data> + <data name="description" xsi:type="string">Check taxed FPT display set to Excluding, Description and Including FPT on product with custom option and catalog price Including Tax</data> <data name="configData" xsi:type="string">shipping_tax_class_taxable_goods,tax_with_fpt_taxed_cat_incl_disc_on_excl</data> <data name="productData" xsi:type="string">with_custom_option_and_fpt</data> <data name="prices/category_price" xsi:type="string">64.67</data> diff --git a/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendAccountEditObserverTest.php b/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendAccountEditObserverTest.php new file mode 100644 index 0000000000000..c09211b020b30 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendAccountEditObserverTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Captcha\Observer; + +use Magento\Captcha\Model\ResourceModel\Log as CaptchaLog; +use Magento\Captcha\Model\ResourceModel\LogFactory; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Class ResetAttemptForFrontendAccountEditObserverTest + * + * Test for checking that the customer login attempts are removed after account details edit + */ +class ResetAttemptForFrontendAccountEditObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + public function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoDataFixture Magento/Captcha/_files/failed_logins_frontend.php + */ + public function testAccountEditRemovesFailedAttempts() + { + $customerEmail = 'mageuser@dummy.com'; + $captchaLogFactory = $this->objectManager->get(LogFactory::class); + $eventManager = $this->objectManager->get(ManagerInterface::class); + + $eventManager->dispatch( + 'customer_account_edited', + ['email' => $customerEmail] + ); + + /** + * @var CaptchaLog $captchaLog + */ + $captchaLog = $captchaLogFactory->create(); + + self::assertEquals(0, $captchaLog->countAttemptsByUserLogin($customerEmail)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php b/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php new file mode 100644 index 0000000000000..f8dd80595f936 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Captcha\Observer; + +use Magento\Captcha\Model\ResourceModel\Log as CaptchaLog; +use Magento\Captcha\Model\ResourceModel\LogFactory; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\CustomerFactory; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Class ResetAttemptForFrontendObserverTest + * + * Test for checking that the customer login attempts are removed after a successful login + */ +class ResetAttemptForFrontendObserverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + public function setUp() + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoDataFixture Magento/Captcha/_files/failed_logins_frontend.php + */ + public function testSuccesfulLoginRemovesFailedAttempts() + { + $customerEmail = 'mageuser@dummy.com'; + $customerFactory = $this->objectManager->get(CustomerFactory::class); + $captchaLogFactory = $this->objectManager->get(LogFactory::class); + $eventManager = $this->objectManager->get(ManagerInterface::class); + + /** @var Customer $customer */ + $customer = $customerFactory->create(); + $customer->setEmail($customerEmail); + + $eventManager->dispatch( + 'customer_customer_authenticated', + ['model' => $customer, 'password' => 'some_password'] + ); + + /** + * @var CaptchaLog $captchaLog + */ + $captchaLog = $captchaLogFactory->create(); + + self::assertEquals(0, $captchaLog->countAttemptsByUserLogin($customerEmail)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Captcha/_files/failed_logins_frontend.php b/dev/tests/integration/testsuite/Magento/Captcha/_files/failed_logins_frontend.php new file mode 100644 index 0000000000000..4e0db30fa82c1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Captcha/_files/failed_logins_frontend.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Captcha\Model\ResourceModel\LogFactory; +use Magento\Captcha\Model\ResourceModel\Log; + +$objectManager = Bootstrap::getObjectManager(); +$logFactory = $objectManager->get(LogFactory::class); + +/** @var Log $captchaLog */ +$captchaLog = $logFactory->create(); +$captchaLog->logAttempt('mageuser@dummy.com'); diff --git a/dev/tests/integration/testsuite/Magento/Captcha/_files/failed_logins_frontend_rollback.php b/dev/tests/integration/testsuite/Magento/Captcha/_files/failed_logins_frontend_rollback.php new file mode 100644 index 0000000000000..a01edb6d0a219 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Captcha/_files/failed_logins_frontend_rollback.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Captcha\Model\ResourceModel\LogFactory; +use Magento\Captcha\Model\ResourceModel\Log; + +$objectManager = Bootstrap::getObjectManager(); +$logFactory = $objectManager->get(LogFactory::class); + +/** @var Log $captchaLog */ +$captchaLog = $logFactory->create(); +$captchaLog->deleteUserAttempts('mageuser@dummy.com'); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/UploaderTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/UploaderTest.php index cf5cb69ebdc27..f61aa7578d4a3 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/UploaderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/UploaderTest.php @@ -48,7 +48,14 @@ protected function setUp() $mediaPath = $appParams[DirectoryList::MEDIA][DirectoryList::PATH]; $this->directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $tmpDir = $this->directory->getRelativePath($mediaPath . '/import'); - $this->uploader->setTmpDir($tmpDir); + if (!$this->directory->create($tmpDir)) { + throw new \RuntimeException('Failed to create temporary directory'); + } + if (!$this->uploader->setTmpDir($tmpDir)) { + throw new \RuntimeException( + 'Failed to set temporary directory for files.' + ); + } parent::setUp(); } @@ -70,6 +77,7 @@ public function testMoveWithValidFile(): void * @magentoAppIsolation enabled * @return void * @expectedException \Exception + * @expectedExceptionMessage Disallowed file type */ public function testMoveWithInvalidFile(): void { diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_two_stores.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_two_stores.csv index f343cd20ecc78..eda9f3a01bf55 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_two_stores.csv +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_two_stores.csv @@ -1,4 +1,4 @@ product_websites,store_view_code,attribute_set_code,product_type,categories,sku,price,name,url_key -base,,Default,simple,Default Category/category-defaultstore,product,123,product,product +base,,Default,simple,Default Category/category-admin,product,123,product,product ,default,Default,simple,,product,,product-default,product-default ,fixturestore,Default,simple,,product,,product-fixture,product-fixture diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php index bbcbc40dc6640..fa49a7b1f57e1 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFilesTest.php @@ -101,11 +101,7 @@ public function testExecuteWithWrongFileName() $this->model->getStorage()->getSession()->setCurrentPath($this->fullDirectoryPath); $this->model->execute(); - $this->assertTrue( - $this->mediaDirectory->isExist( - $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . $fileName) - ) - ); + $this->assertFileExists($this->fullDirectoryPath . $fileName); } /** diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php index 8e30e85541a42..a1a29706756b5 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolderTest.php @@ -112,11 +112,7 @@ public function testExecuteWithWrongDirectoryName() $this->model->getRequest()->setParams(['node' => $this->imagesHelper->idEncode($directoryName)]); $this->model->execute(); - $this->assertTrue( - $this->mediaDirectory->isExist( - $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . $directoryName) - ) - ); + $this->assertFileExists($this->fullDirectoryPath . $directoryName); } /** diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolderTest.php index 0c74f18e9c44a..e509737a0020f 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolderTest.php @@ -111,10 +111,8 @@ public function testExecuteWithWrongPath() $this->model->getStorage()->getSession()->setCurrentPath($this->fullDirectoryPath . $dirPath); $this->model->execute(); - $this->assertFalse( - $this->mediaDirectory->isExist( - $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . $dirPath . $this->dirName) - ) + $this->assertFileNotExists( + $this->fullDirectoryPath . $dirPath . $this->dirName ); } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php index 534eb3db35b3f..bab14a8663eae 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/UploadTest.php @@ -125,10 +125,8 @@ public function testExecuteWithWrongPath() $this->model->getStorage()->getSession()->setCurrentPath($dirPath); $this->model->execute(); - $this->assertFalse( - $this->mediaDirectory->isExist( - $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . $dirPath . $this->fileName) - ) + $this->assertFileNotExists( + $this->fullDirectoryPath . $dirPath . $this->fileName ); } @@ -147,11 +145,7 @@ public function testExecuteWithWrongFileName() $this->model->getStorage()->getSession()->setCurrentPath($this->fullDirectoryPath); $this->model->execute(); - $this->assertFalse( - $this->mediaDirectory->isExist( - $this->mediaDirectory->getRelativePath($this->fullDirectoryPath . $newFilename) - ) - ); + $this->assertFileNotExists($this->fullDirectoryPath . $newFilename); } /** diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/ValidatorTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/ValidatorTest.php index b65aa7734f2da..45d84336337c5 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/ValidatorTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/ValidatorTest.php @@ -130,7 +130,7 @@ public function getPostcodesDataProvider() ['countryId' => 'IS', 'postcode' => '123'], ['countryId' => 'IN', 'postcode' => '123456'], ['countryId' => 'ID', 'postcode' => '12345'], - ['countryId' => 'IL', 'postcode' => '12345'], + ['countryId' => 'IL', 'postcode' => '1234567'], ['countryId' => 'IT', 'postcode' => '12345'], ['countryId' => 'JP', 'postcode' => '123-4567'], ['countryId' => 'JP', 'postcode' => '1234567'], diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/DirectoryResolverTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/DirectoryResolverTest.php index 8f1f9e92972e5..90d8473032c69 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/DirectoryResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/DirectoryResolverTest.php @@ -52,7 +52,7 @@ public function testValidatePath($path, $directoryConfig, $expectation) { $directory = $this->filesystem ->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); - $path = $directory->getAbsolutePath($path); + $path = $directory->getAbsolutePath() .'/' .$path; $this->assertEquals($expectation, $this->directoryResolver->validatePath($path, $directoryConfig)); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php index d74a83c339326..a43013860c79c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php @@ -5,12 +5,13 @@ */ namespace Magento\Framework\Code; -use Magento\Framework\Code\Generator; +use Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceFactoryGenerator; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; use Magento\Framework\Interception\Code\Generator as InterceptionGenerator; use Magento\Framework\ObjectManager\Code\Generator as DIGenerator; -use Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceFactoryGenerator; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; require_once __DIR__ . '/GeneratorTest/SourceClassWithNamespace.php'; require_once __DIR__ . '/GeneratorTest/ParentClassWithNamespace.php'; @@ -18,60 +19,77 @@ /** * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class GeneratorTest extends \PHPUnit\Framework\TestCase +class GeneratorTest extends TestCase { - const CLASS_NAME_WITH_NAMESPACE = \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespace::class; + const CLASS_NAME_WITH_NAMESPACE = GeneratorTest\SourceClassWithNamespace::class; /** - * @var \Magento\Framework\Code\Generator + * @var Generator */ protected $_generator; /** - * @var \Magento\Framework\Code\Generator\Io + * @var Generator/Io */ protected $_ioObject; /** - * @var \Magento\Framework\Filesystem\Directory\Write + * @var Filesystem\Directory\Write + */ + private $generatedDirectory; + + /** + * @var Filesystem\Directory\Read + */ + private $logDirectory; + + /** + * @var string */ - protected $varDirectory; + private $testRelativePath = './Magento/Framework/Code/GeneratorTest/'; + /** + * @inheritdoc + */ protected function setUp() { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->varDirectory = $objectManager->get( - \Magento\Framework\Filesystem::class - )->getDirectoryWrite( - DirectoryList::VAR_DIR - ); - $generationDirectory = $this->varDirectory->getAbsolutePath('generation'); - $this->_ioObject = new \Magento\Framework\Code\Generator\Io( - new \Magento\Framework\Filesystem\Driver\File(), - $generationDirectory - ); + $objectManager = Bootstrap::getObjectManager(); + /** @var Filesystem $filesystem */ + $filesystem = $objectManager->get(Filesystem::class); + $this->generatedDirectory = $filesystem->getDirectoryWrite(DirectoryList::GENERATED_CODE); + $this->logDirectory = $filesystem->getDirectoryRead(DirectoryList::LOG); + $generatedDirectoryAbsolutePath = $this->generatedDirectory->getAbsolutePath(); + $this->_ioObject = new Generator\Io(new Filesystem\Driver\File(), $generatedDirectoryAbsolutePath); $this->_generator = $objectManager->create( - \Magento\Framework\Code\Generator::class, + Generator::class, [ 'ioObject' => $this->_ioObject, 'generatedEntities' => [ ExtensionAttributesInterfaceFactoryGenerator::ENTITY_TYPE => ExtensionAttributesInterfaceFactoryGenerator::class, - DIGenerator\Factory::ENTITY_TYPE => \Magento\Framework\ObjectManager\Code\Generator\Factory::class, - DIGenerator\Proxy::ENTITY_TYPE => \Magento\Framework\ObjectManager\Code\Generator\Proxy::class, - InterceptionGenerator\Interceptor::ENTITY_TYPE => - \Magento\Framework\Interception\Code\Generator\Interceptor::class, + DIGenerator\Factory::ENTITY_TYPE => DIGenerator\Factory::class, + DIGenerator\Proxy::ENTITY_TYPE => DIGenerator\Proxy::class, + InterceptionGenerator\Interceptor::ENTITY_TYPE => InterceptionGenerator\Interceptor::class, ] ] ); $this->_generator->setObjectManager($objectManager); } + /** + * @inheritdoc + */ protected function tearDown() { - $this->varDirectory->delete('generation'); $this->_generator = null; + if ($this->generatedDirectory->isExist($this->testRelativePath)) { + if (!$this->generatedDirectory->isWritable($this->testRelativePath)) { + $this->generatedDirectory->changePermissionsRecursively($this->testRelativePath, 0775, 0664); + } + $this->generatedDirectory->delete($this->testRelativePath); + } } protected function _clearDocBlock($classBody) @@ -79,81 +97,59 @@ protected function _clearDocBlock($classBody) return preg_replace('/(\/\*[\w\W]*)\nclass/', 'class', $classBody); } + /** + * Generates a new file with Factory class and compares with the sample from the + * SourceClassWithNamespaceFactory.php.sample file. + */ public function testGenerateClassFactoryWithNamespace() { $factoryClassName = self::CLASS_NAME_WITH_NAMESPACE . 'Factory'; - $result = false; - $generatorResult = $this->_generator->generateClass($factoryClassName); - if (\Magento\Framework\Code\Generator::GENERATION_ERROR !== $generatorResult) { - $result = true; - } - $this->assertTrue($result, 'Failed asserting that \'' . (string)$generatorResult . '\' equals \'success\'.'); - - $factory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create($factoryClassName); - - $object = $factory->create(); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE, $object); - - // This test is only valid if the factory created the object if Autoloader did not pick it up automatically - if (\Magento\Framework\Code\Generator::GENERATION_SUCCESS == $generatorResult) { - $content = $this->_clearDocBlock( - file_get_contents( - $this->_ioObject->generateResultFileName(self::CLASS_NAME_WITH_NAMESPACE . 'Factory') - ) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceFactory.php.sample') - ); - $this->assertEquals($expectedContent, $content); - } + $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); + $factory = Bootstrap::getObjectManager()->create($factoryClassName); + $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE, $factory->create()); + $content = $this->_clearDocBlock( + file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) + ); + $expectedContent = $this->_clearDocBlock( + file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceFactory.php.sample') + ); + $this->assertEquals($expectedContent, $content); } + /** + * Generates a new file with Proxy class and compares with the sample from the + * SourceClassWithNamespaceProxy.php.sample file. + */ public function testGenerateClassProxyWithNamespace() { $proxyClassName = self::CLASS_NAME_WITH_NAMESPACE . '\Proxy'; - $result = false; - $generatorResult = $this->_generator->generateClass($proxyClassName); - if (\Magento\Framework\Code\Generator::GENERATION_ERROR !== $generatorResult) { - $result = true; - } - $this->assertTrue($result, 'Failed asserting that \'' . (string)$generatorResult . '\' equals \'success\'.'); - - $proxy = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create($proxyClassName); + $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($proxyClassName)); + $proxy = Bootstrap::getObjectManager()->create($proxyClassName); $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE, $proxy); - - // This test is only valid if the factory created the object if Autoloader did not pick it up automatically - if (\Magento\Framework\Code\Generator::GENERATION_SUCCESS == $generatorResult) { - $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName(self::CLASS_NAME_WITH_NAMESPACE . '\Proxy')) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceProxy.php.sample') - ); - $this->assertEquals($expectedContent, $content); - } + $content = $this->_clearDocBlock( + file_get_contents($this->_ioObject->generateResultFileName($proxyClassName)) + ); + $expectedContent = $this->_clearDocBlock( + file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceProxy.php.sample') + ); + $this->assertEquals($expectedContent, $content); } + /** + * Generates a new file with Interceptor class and compares with the sample from the + * SourceClassWithNamespaceInterceptor.php.sample file. + */ public function testGenerateClassInterceptorWithNamespace() { $interceptorClassName = self::CLASS_NAME_WITH_NAMESPACE . '\Interceptor'; - $result = false; - $generatorResult = $this->_generator->generateClass($interceptorClassName); - if (\Magento\Framework\Code\Generator::GENERATION_ERROR !== $generatorResult) { - $result = true; - } - $this->assertTrue($result, 'Failed asserting that \'' . (string)$generatorResult . '\' equals \'success\'.'); - - if (\Magento\Framework\Code\Generator::GENERATION_SUCCESS == $generatorResult) { - $content = $this->_clearDocBlock( - file_get_contents( - $this->_ioObject->generateResultFileName(self::CLASS_NAME_WITH_NAMESPACE . '\Interceptor') - ) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceInterceptor.php.sample') - ); - $this->assertEquals($expectedContent, $content); - } + $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($interceptorClassName)); + $content = $this->_clearDocBlock( + file_get_contents($this->_ioObject->generateResultFileName($interceptorClassName)) + ); + $expectedContent = $this->_clearDocBlock( + file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceInterceptor.php.sample') + ); + $this->assertEquals($expectedContent, $content); } /** @@ -163,26 +159,35 @@ public function testGenerateClassInterceptorWithNamespace() public function testGenerateClassExtensionAttributesInterfaceFactoryWithNamespace() { $factoryClassName = self::CLASS_NAME_WITH_NAMESPACE . 'ExtensionInterfaceFactory'; - $this->varDirectory->create( - $this->varDirectory->getAbsolutePath('generation') . '/Magento/Framework/Code/GeneratorTest/' - ); - - $generatorResult = $this->_generator->generateClass($factoryClassName); - + $this->generatedDirectory->create($this->testRelativePath); + $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); $factory = Bootstrap::getObjectManager()->create($factoryClassName); - $object = $factory->create(); - - $this->assertEquals($generatorResult, Generator::GENERATION_SUCCESS); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE . 'Extension', $object); - + $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE . 'Extension', $factory->create()); $content = $this->_clearDocBlock( - file_get_contents( - $this->_ioObject->generateResultFileName(self::CLASS_NAME_WITH_NAMESPACE . 'ExtensionInterfaceFactory') - ) + file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) ); $expectedContent = $this->_clearDocBlock( file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceExtensionInterfaceFactory.php.sample') ); $this->assertEquals($expectedContent, $content); } + + /** + * It tries to generate a new class file when the generated directory is read-only + */ + public function testGeneratorClassWithErrorSaveClassFile() + { + $factoryClassName = self::CLASS_NAME_WITH_NAMESPACE . 'Factory'; + $msgPart = 'Class ' . $factoryClassName . ' generation error: The requested class did not generate properly, ' + . 'because the \'generated\' directory permission is read-only.'; + $regexpMsgPart = preg_quote($msgPart); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessageRegExp("/.*$regexpMsgPart.*/"); + $this->generatedDirectory->create($this->testRelativePath); + $this->generatedDirectory->changePermissionsRecursively($this->testRelativePath, 0555, 0444); + $generatorResult = $this->_generator->generateClass($factoryClassName); + $this->assertFalse($generatorResult); + $pathToSystemLog = $this->logDirectory->getAbsolutePath('system.log'); + $this->assertContains($msgPart, file_get_contents($pathToSystemLog)); + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/ReadTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/ReadTest.php index f43a523a12ed0..bc77eeb932c9a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/ReadTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/ReadTest.php @@ -7,6 +7,7 @@ */ namespace Magento\Framework\Filesystem\Directory; +use Magento\Framework\Exception\ValidatorException; use Magento\TestFramework\Helper\Bootstrap; /** @@ -34,13 +35,66 @@ public function testGetAbsolutePath() $this->assertContains('_files/foo/bar', $dir->getAbsolutePath('bar')); } + public function testGetAbsolutePathOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->getAbsolutePath('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->getAbsolutePath('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->getAbsolutePath('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + public function testGetRelativePath() { $dir = $this->getDirectoryInstance('foo'); + $this->assertEquals( + 'file_three.txt', + $dir->getRelativePath('file_three.txt') + ); $this->assertEquals('', $dir->getRelativePath()); $this->assertEquals('bar', $dir->getRelativePath(__DIR__ . '/../_files/foo/bar')); } + public function testGetRelativePathOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->getRelativePath(__DIR__ .'/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->getRelativePath(__DIR__ .'//./..////Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->getRelativePath(__DIR__ .'\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->getRelativePath('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(4, $exceptions); + } + /** * Test for read method * @@ -72,6 +126,28 @@ public function readProvider() ]; } + public function testReadOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->read('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->read('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->read('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for search method * @@ -103,6 +179,28 @@ public function searchProvider() ]; } + public function testSearchOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->search('/*/*.txt', '../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->search('/*/*.txt', '//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->search('/*/*.txt', '\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for isExist method * @@ -127,6 +225,28 @@ public function existsProvider() return [['foo', 'bar', true], ['foo', 'bar/baz/', true], ['foo', 'bar/notexists', false]]; } + public function testIsExistOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->isExist('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isExist('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isExist('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for stat method * @@ -168,6 +288,28 @@ public function statProvider() return [['foo', 'bar'], ['foo', 'file_three.txt']]; } + public function testStatOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->stat('bar/../../../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->stat('bar//./..///../../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->stat('bar\..\..\..\Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for isReadable method * @@ -182,6 +324,28 @@ public function testIsReadable($dirPath, $path, $readable) $this->assertEquals($readable, $dir->isReadable($path)); } + public function testIsReadableOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->isReadable('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isReadable('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isReadable('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for isFile method * @@ -194,6 +358,28 @@ public function testIsFile($path, $isFile) $this->assertEquals($isFile, $this->getDirectoryInstance('foo')->isFile($path)); } + public function testIsFileOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->isFile('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isFile('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isFile('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for isDirectory method * @@ -206,6 +392,28 @@ public function testIsDirectory($path, $isDirectory) $this->assertEquals($isDirectory, $this->getDirectoryInstance('foo')->isDirectory($path)); } + public function testIsDirectoryOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->isDirectory('../../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isDirectory('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isDirectory('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Data provider for testIsReadable * @@ -246,6 +454,28 @@ public function testOpenFile() $this->assertTrue($file instanceof \Magento\Framework\Filesystem\File\ReadInterface); } + public function testOpenFileOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->openFile('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->openFile('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->openFile('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test readFile * @@ -268,10 +498,35 @@ public function readFileProvider() { return [ ['popup.csv', 'var myData = 5;'], - ['data.csv', '"field1", "field2"' . "\n" . '"field3", "field4"' . "\n"] + [ + 'data.csv', + '"field1", "field2"' . PHP_EOL . '"field3", "field4"' . PHP_EOL + ] ]; } + public function testReadFileOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->readFile('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->readFile('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->readFile('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Get readable file instance * Get full path for files located in _files directory @@ -301,4 +556,26 @@ public function testReadRecursively() sort($expected); $this->assertEquals($expected, $actual); } + + public function testReadRecursivelyOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('foo'); + try { + $dir->readRecursively('../../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->readRecursively('//./..///../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->readRecursively('\..\..\Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php index 380c7998680a1..39a224ff902d5 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php @@ -7,6 +7,7 @@ */ namespace Magento\Framework\Filesystem\Directory; +use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem\DriverPool; use Magento\TestFramework\Helper\Bootstrap; @@ -63,6 +64,28 @@ public function createProvider() ]; } + public function testCreateOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->create('../../outsideDir'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->create('//./..///../outsideDir'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->create('\..\..\outsideDir'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for delete method * @@ -88,6 +111,28 @@ public function deleteProvider() return [['subdir'], ['subdir/subsubdir']]; } + public function testDeleteOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->delete('../../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->delete('//./..///../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->delete('\..\..\Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for rename method (in scope of one directory instance) * @@ -119,6 +164,31 @@ public function renameProvider() return [['newDir1', 0777, 'first_name.txt', 'second_name.txt']]; } + public function testRenameOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->renameFile('../../Directory/ReadTest.php', 'RenamedTest'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->renameFile( + '//./..///../Directory/ReadTest.php', + 'RenamedTest' + ); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->renameFile('\..\..\Directory\ReadTest.php', 'RenamedTest'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for rename method (moving to new directory instance) * @@ -185,6 +255,40 @@ public function copyProvider() ]; } + public function testCopyOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + $dir->touch('test_file_for_copy_outside.txt'); + try { + $dir->copyFile('../../Directory/ReadTest.php', 'CopiedTest'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->copyFile( + '//./..///../Directory/ReadTest.php', + 'CopiedTest' + ); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->copyFile('\..\..\Directory\ReadTest.php', 'CopiedTest'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->copyFile( + 'test_file_for_copy_outside.txt', + '../../Directory/copied_outside.txt' + ); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(4, $exceptions); + } + /** * Test for copy method (copy to another directory instance) * @@ -231,6 +335,28 @@ public function testChangePermissions() $this->assertTrue($directory->changePermissions('test_directory', 0644)); } + public function testChangePermissionsOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->changePermissions('../../Directory', 0777); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->changePermissions('//./..///../Directory', 0777); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->changePermissions('\..\..\Directory', 0777); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for changePermissionsRecursively method */ @@ -244,6 +370,28 @@ public function testChangePermissionsRecursively() $this->assertTrue($directory->changePermissionsRecursively('test_directory', 0777, 0644)); } + public function testChangePermissionsRecursivelyOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->changePermissionsRecursively('../foo', 0777, 0777); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->changePermissionsRecursively('//./..///foo', 0777, 0777); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->changePermissionsRecursively('\..\foo', 0777, 0777); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for touch method * @@ -274,6 +422,28 @@ public function touchProvider() ]; } + public function testTouchOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->touch('../../foo.tst'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->touch('//./..///../foo.tst'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->touch('\..\..\foo.tst'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test isWritable method */ @@ -285,6 +455,28 @@ public function testIsWritable() $this->assertTrue($directory->isWritable('bar')); } + public function testIsWritableOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->isWritable('../../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isWritable('//./..///../Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->isWritable('\..\..\Directory'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test for openFile method * @@ -315,6 +507,28 @@ public function openFileProvider() ]; } + public function testOpenFileOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->openFile('../../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->openFile('//./..///../Directory/ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->openFile('\..\..\Directory\ReadTest.php'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Test writeFile * @@ -359,6 +573,28 @@ public function writeFileProvider() return [['file1', '123', '456'], ['folder1/file1', '123', '456']]; } + public function testWriteFileOutside() + { + $exceptions = 0; + $dir = $this->getDirectoryInstance('newDir1', 0777); + try { + $dir->writeFile('../../Directory/ReadTest.php', 'tst'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->writeFile('//./..///../Directory/ReadTest.php', 'tst'); + } catch (ValidatorException $exception) { + $exceptions++; + } + try { + $dir->writeFile('\..\..\Directory\ReadTest.php', 'tst'); + } catch (ValidatorException $exception) { + $exceptions++; + } + $this->assertEquals(3, $exceptions); + } + /** * Tear down */ diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/TruncateFilterTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/TruncateFilterTest.php new file mode 100644 index 0000000000000..3736012848379 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/TruncateFilterTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filter; + +class TruncateFilterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @param string $expectedValue + * @param string $expectedRemainder + * @param string $string + * @param int $length + * @param string $etc + * @param bool $breakWords + * @dataProvider truncateDataProvider + */ + public function testFilter( + $expectedValue, + $expectedRemainder, + $string, + $length = 5, + $etc = '...', + $breakWords = true + ) { + /** @var TruncateFilter $truncateFilter */ + $truncateFilter = \Magento\TestFramework\ObjectManager::getInstance()->create( + TruncateFilter::class, + [ + 'length' => $length, + 'etc' => $etc, + 'breakWords' => $breakWords, + ] + ); + $result = $truncateFilter->filter($string); + $this->assertEquals($expectedValue, $result->getValue()); + $this->assertEquals($expectedRemainder, $result->getRemainder()); + } + + public function truncateDataProvider() : array + { + return [ + '1' => [ + '12...', + '34567890', + '1234567890', + ], + '2' => [ + '123..', + ' 456 789', + '123 456 789', + 8, + '..', + false + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/schemaA.graphqls b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/schemaA.graphqls index d736bb4fa26f1..6c832e5f122a6 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/schemaA.graphqls +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/_files/schemaA.graphqls @@ -2,6 +2,10 @@ type Query { placeholder: String @doc(description: "comment for placeholder.") } +type Mutation { + placeholder: String @doc(description: "comment for placeholder.") +} + input FilterTypeInput @doc(description:"Comment for FilterTypeInput") { eq: String @doc(description:"Equal") finset: [String] diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php index 2f798deecd9d5..6d5da7243ffbe 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/ObjectManagerTest.php @@ -7,6 +7,11 @@ class ObjectManagerTest extends \PHPUnit\Framework\TestCase { + /**#@+ + * Test class with type error + */ + const TEST_CLASS_WITH_TYPE_ERROR = \Magento\Framework\ObjectManager\TestAsset\ConstructorWithTypeError::class; + /**#@+ * Test classes for basic instantiation */ @@ -138,4 +143,17 @@ public function testNewInstance($actualClassName, array $properties = [], $expec } } } + + /** + * Test creating an object and passing incorrect type of arguments to the constructor. + * + * @expectedException \Magento\Framework\Exception\RuntimeException + * @expectedExceptionMessage Error occurred when creating object + */ + public function testNewInstanceWithTypeError() + { + self::$_objectManager->create(self::TEST_CLASS_WITH_TYPE_ERROR, [ + 'testArgument' => new \stdClass() + ]); + } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/TestAsset/ConstructorWithTypeError.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/TestAsset/ConstructorWithTypeError.php new file mode 100644 index 0000000000000..92d16e4df3511 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/TestAsset/ConstructorWithTypeError.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\ObjectManager\TestAsset; + +/** + * Test asset used to test invalid argument types on the constructor invocation. + */ +class ConstructorWithTypeError +{ + /** + * @var Basic + */ + private $testArgument; + + /** + * @param Basic $testArgument + */ + public function __construct(Basic $testArgument) + { + $this->testArgument = $testArgument; + } +} diff --git a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php index f362e75ea790e..4acba63d98e66 100644 --- a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php +++ b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php @@ -130,7 +130,7 @@ public function testCheckThatPidFilesWasCreated() public function testSpecificConsumerAndRerun() { $specificConsumer = 'quoteItemCleaner'; - $pidFilePath = $specificConsumer . ConsumersRunner::PID_FILE_EXT; + $pidFilePath = $this->getPidFileName($specificConsumer); $config = $this->config; $config['cron_consumers_runner'] = ['consumers' => [$specificConsumer], 'max_messages' => 0]; @@ -228,7 +228,7 @@ private function writeConfig(array $config) private function getPidFileFullPath($consumerName) { $directoryList = $this->objectManager->get(DirectoryList::class); - return $directoryList->getPath(DirectoryList::VAR_DIR) . '/' . $consumerName . ConsumersRunner::PID_FILE_EXT; + return $directoryList->getPath(DirectoryList::VAR_DIR) . '/' . $this->getPidFileName($consumerName); } /** @@ -239,7 +239,7 @@ protected function tearDown() foreach ($this->consumerConfig->getConsumers() as $consumer) { $consumerName = $consumer->getName(); $pidFileFullPath = $this->getPidFileFullPath($consumerName); - $pidFilePath = $consumerName . ConsumersRunner::PID_FILE_EXT; + $pidFilePath = $this->getPidFileName($consumerName); $pid = $this->pid->getPid($pidFilePath); if ($pid && $this->pid->isRun($pidFilePath)) { @@ -258,4 +258,15 @@ protected function tearDown() $this->writeConfig($this->config); $this->appConfig->reinit(); } + + /** + * @param string $consumerName The consumers name + * @return string The name to file with PID + */ + private function getPidFileName($consumerName) + { + $sanitizedHostname = preg_replace('/[^a-z0-9]/i', '', gethostname()); + + return $consumerName . '-' . $sanitizedHostname . ConsumersRunner::PID_FILE_EXT; + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Items/Column/NameTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Items/Column/NameTest.php new file mode 100644 index 0000000000000..26bdbf3a6cfde --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Items/Column/NameTest.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Items\Column; + +/** + * @magentoAppArea adminhtml + */ +class NameTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Name + */ + private $block; + + protected function setUp() + { + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var $layout \Magento\Framework\View\Layout */ + $layout = $objectManager->create(\Magento\Framework\View\LayoutInterface::class); + /** @var $block \Magento\Sales\Block\Adminhtml\Items\AbstractItems */ + $this->block = $layout->createBlock(Name::class, 'block'); + } + + public function testTruncateString() : void + { + $remainder = ''; + $this->assertEquals( + '12345', + $this->block->truncateString('1234567890', 5, '', $remainder) + ); + } + + public function testGetFormattedOptiong() : void + { + $this->assertEquals( + [ + 'value' => '1234567890123456789012345678901234567890123456789012345', + 'remainder' => '67890', + ], + $this->block->getFormattedOption( + '123456789012345678901234567890123456789012345678901234567890' + ) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php index 1ea2b28986d8a..95fcf24894c87 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Controller/Adminhtml/System/Design/Config/SaveTest.php @@ -14,6 +14,11 @@ */ class SaveTest extends AbstractBackendController { + /** + * @var FormKey + */ + private $formKey; + /** * @inheritdoc */ @@ -24,6 +29,15 @@ class SaveTest extends AbstractBackendController */ protected $uri = 'backend/theme/design_config/save'; + protected function setUp() + { + parent::setUp(); + + $this->formKey = $this->_objectManager->get( + FormKey::class + ); + } + /** * Test design configuration save valid values. * @@ -89,7 +103,22 @@ private function getRequestParams() 'watermark_swatch_image_imageOpacity' => '', 'watermark_swatch_image_position' => 'stretch', 'scope' => 'default', - 'form_key' => $this->_objectManager->get(FormKey::class)->getFormKey(), + 'form_key' => $this->formKey->getFormKey(), ]; } + + public function testAclHasAccess() + { + $this->getRequest()->setMethod( + \Zend\Http\Request::METHOD_POST + ); + + $this->getRequest()->setParams( + [ + 'form_key' => $this->formKey->getFormKey() + ] + ); + + parent::testAclHasAccess(); + } } diff --git a/dev/travis/before_script.sh b/dev/travis/before_script.sh index 7cf55ca8083f1..f2ba20dc412b6 100755 --- a/dev/travis/before_script.sh +++ b/dev/travis/before_script.sh @@ -13,8 +13,8 @@ case $TEST_SUITE in test_set_list=$(find testsuite/* -maxdepth 1 -mindepth 1 -type d | sort) test_set_count=$(printf "$test_set_list" | wc -l) - test_set_size[1]=$(printf "%.0f" $(echo "$test_set_count*0.12" | bc)) #12% - test_set_size[2]=$(printf "%.0f" $(echo "$test_set_count*0.32" | bc)) #32% + test_set_size[1]=$(printf "%.0f" $(echo "$test_set_count*0.17" | bc)) #17% + test_set_size[2]=$(printf "%.0f" $(echo "$test_set_count*0.27" | bc)) #27% test_set_size[3]=$((test_set_count-test_set_size[1]-test_set_size[2])) #56% echo "Total = ${test_set_count}; Batch #1 = ${test_set_size[1]}; Batch #2 = ${test_set_size[2]}; Batch #3 = ${test_set_size[3]};"; diff --git a/lib/internal/Magento/Framework/Api/SortOrder.php b/lib/internal/Magento/Framework/Api/SortOrder.php index 53bb5a87451c2..67897ea22570d 100644 --- a/lib/internal/Magento/Framework/Api/SortOrder.php +++ b/lib/internal/Magento/Framework/Api/SortOrder.php @@ -25,6 +25,7 @@ class SortOrder extends AbstractSimpleObject * Initialize object and validate sort direction * * @param array $data + * @throws InputException */ public function __construct(array $data = []) { @@ -32,6 +33,9 @@ public function __construct(array $data = []) if (null !== $this->getDirection()) { $this->validateDirection($this->getDirection()); } + if ($this->getField() !== null) { + $this->validateField($this->getField()); + } } /** @@ -48,10 +52,14 @@ public function getField() * Set sorting field. * * @param string $field + * @throws InputException + * * @return $this */ public function setField($field) { + $this->validateField($field); + return $this->setData(SortOrder::FIELD, $field); } @@ -69,6 +77,8 @@ public function getDirection() * Set sorting direction. * * @param string $direction + * @throws InputException + * * @return $this */ public function setDirection($direction) @@ -81,10 +91,10 @@ public function setDirection($direction) * Validate direction argument ASC or DESC * * @param mixed $direction - * @return null + * @return void * @throws InputException */ - private function validateDirection($direction) + private function validateDirection($direction): void { $this->validateDirectionIsString($direction); $this->validateDirectionIsAscOrDesc($direction); @@ -93,9 +103,9 @@ private function validateDirection($direction) /** * @param string $direction * @throws InputException - * @return null + * @return void */ - private function validateDirectionIsString($direction) + private function validateDirectionIsString($direction): void { if (!is_string($direction)) { throw new InputException(new Phrase( @@ -108,9 +118,9 @@ private function validateDirectionIsString($direction) /** * @param string $direction * @throws InputException - * @return null + * @return void */ - private function validateDirectionIsAscOrDesc($direction) + private function validateDirectionIsAscOrDesc($direction): void { $normalizedDirection = $this->normalizeDirectionInput($direction); if (!in_array($normalizedDirection, [SortOrder::SORT_ASC, SortOrder::SORT_DESC], true)) { @@ -129,4 +139,23 @@ private function normalizeDirectionInput($direction) { return strtoupper($direction); } + + /** + * Check if given value can be used as sorting field. + * + * @param string $field + * @return void + * @throws InputException + */ + private function validateField(string $field): void + { + if (preg_match('/[^a-z0-9\_]/i', $field)) { + throw new InputException( + new Phrase( + 'Sort order field %1 contains restricted symbols', + [$field] + ) + ); + } + } } diff --git a/lib/internal/Magento/Framework/Api/Test/Unit/SortOrderTest.php b/lib/internal/Magento/Framework/Api/Test/Unit/SortOrderTest.php index c2ecf51e8afc4..31a07c97d782d 100644 --- a/lib/internal/Magento/Framework/Api/Test/Unit/SortOrderTest.php +++ b/lib/internal/Magento/Framework/Api/Test/Unit/SortOrderTest.php @@ -92,4 +92,14 @@ public function testItValidatesADirectionAssignedDuringInstantiation() SortOrder::DIRECTION => 'not-asc-or-desc' ]); } + + /** + * @expectedException \Magento\Framework\Exception\InputException + */ + public function testValidateField() + { + $this->sortOrder = new SortOrder([ + SortOrder::FIELD => 'invalid field (value);' + ]); + } } diff --git a/lib/internal/Magento/Framework/App/Cache/Frontend/Factory.php b/lib/internal/Magento/Framework/App/Cache/Frontend/Factory.php index e8eca51aad976..f2bc4eceb507d 100644 --- a/lib/internal/Magento/Framework/App/Cache/Frontend/Factory.php +++ b/lib/internal/Magento/Framework/App/Cache/Frontend/Factory.php @@ -147,15 +147,17 @@ public function create(array $options) $result = $this->_objectManager->create( \Magento\Framework\Cache\Frontend\Adapter\Zend::class, [ - 'frontend' => \Zend_Cache::factory( - $frontend['type'], - $backend['type'], - $frontend, - $backend['options'], - true, - true, - true - ) + 'frontendFactory' => function () use ($frontend, $backend) { + return \Zend_Cache::factory( + $frontend['type'], + $backend['type'], + $frontend, + $backend['options'], + true, + true, + true + ); + } ] ); $result = $this->_applyDecorators($result); diff --git a/lib/internal/Magento/Framework/App/Test/Unit/BootstrapTest.php b/lib/internal/Magento/Framework/App/Test/Unit/BootstrapTest.php index bfa96a1702879..32e495ed00a82 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/BootstrapTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/BootstrapTest.php @@ -135,7 +135,7 @@ public function testCreateFilesystemDriverPool() ); /** @var \Magento\Framework\Filesystem\DriverPool $result */ $this->assertInstanceOf(\Magento\Framework\Filesystem\DriverPool::class, $result); - $this->assertInstanceof($driverClass, $result->getDriver('custom')); + $this->assertInstanceOf($driverClass, $result->getDriver('custom')); } public function testGetParams() diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Cache/Frontend/FactoryTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Cache/Frontend/FactoryTest.php index e87eca57c058d..48a8420cfda60 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Cache/Frontend/FactoryTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Cache/Frontend/FactoryTest.php @@ -128,7 +128,7 @@ protected function _buildModelForCreate($enforcedOptions = [], $decorators = []) $processFrontendFunc = function ($class, $params) { switch ($class) { case \Magento\Framework\Cache\Frontend\Adapter\Zend::class: - return new $class($params['frontend']); + return new $class($params['frontendFactory']); case \Magento\Framework\App\Test\Unit\Cache\Frontend\FactoryTest\CacheDecoratorDummy::class: $frontend = $params['frontend']; unset($params['frontend']); diff --git a/lib/internal/Magento/Framework/Cache/Frontend/Adapter/Zend.php b/lib/internal/Magento/Framework/Cache/Frontend/Adapter/Zend.php index c8917a099689b..43d261c1ed078 100644 --- a/lib/internal/Magento/Framework/Cache/Frontend/Adapter/Zend.php +++ b/lib/internal/Magento/Framework/Cache/Frontend/Adapter/Zend.php @@ -16,11 +16,27 @@ class Zend implements \Magento\Framework\Cache\FrontendInterface protected $_frontend; /** - * @param \Zend_Cache_Core $frontend + * Factory that creates the \Zend_Cache_Cores + * + * @var \Closure + */ + private $frontendFactory; + + /** + * The pid that owns the $_frontend object + * + * @var int */ - public function __construct(\Zend_Cache_Core $frontend) + private $pid; + + /** + * @param \Closure $frontendFactory + */ + public function __construct(\Closure $frontendFactory) { - $this->_frontend = $frontend; + $this->frontendFactory = $frontendFactory; + $this->_frontend = $frontendFactory(); + $this->pid = getmypid(); } /** @@ -28,7 +44,7 @@ public function __construct(\Zend_Cache_Core $frontend) */ public function test($identifier) { - return $this->_frontend->test($this->_unifyId($identifier)); + return $this->getFrontEnd()->test($this->_unifyId($identifier)); } /** @@ -36,7 +52,7 @@ public function test($identifier) */ public function load($identifier) { - return $this->_frontend->load($this->_unifyId($identifier)); + return $this->getFrontEnd()->load($this->_unifyId($identifier)); } /** @@ -44,7 +60,7 @@ public function load($identifier) */ public function save($data, $identifier, array $tags = [], $lifeTime = null) { - return $this->_frontend->save($data, $this->_unifyId($identifier), $this->_unifyIds($tags), $lifeTime); + return $this->getFrontEnd()->save($data, $this->_unifyId($identifier), $this->_unifyIds($tags), $lifeTime); } /** @@ -52,13 +68,14 @@ public function save($data, $identifier, array $tags = [], $lifeTime = null) */ public function remove($identifier) { - return $this->_frontend->remove($this->_unifyId($identifier)); + return $this->getFrontEnd()->remove($this->_unifyId($identifier)); } /** * {@inheritdoc} * * @throws \InvalidArgumentException Exception is thrown when non-supported cleaning mode is specified + * @throws \Zend_Cache_Exception */ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) { @@ -76,7 +93,7 @@ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) "Magento cache frontend does not support the cleaning mode '{$mode}'." ); } - return $this->_frontend->clean($mode, $this->_unifyIds($tags)); + return $this->getFrontEnd()->clean($mode, $this->_unifyIds($tags)); } /** @@ -84,7 +101,7 @@ public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, array $tags = []) */ public function getBackend() { - return $this->_frontend->getBackend(); + return $this->getFrontEnd()->getBackend(); } /** @@ -92,7 +109,7 @@ public function getBackend() */ public function getLowLevelFrontend() { - return $this->_frontend; + return $this->getFrontEnd(); } /** @@ -119,4 +136,20 @@ protected function _unifyIds(array $ids) } return $ids; } + + /** + * Get frontEnd cache adapter for current pid + * + * @return \Zend_Cache_Core + */ + private function getFrontEnd() + { + if (getmypid() === $this->pid) { + return $this->_frontend; + } + $frontendFactory = $this->frontendFactory; + $this->_frontend = $frontendFactory(); + $this->pid = getmypid(); + return $this->_frontend; + } } diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/Frontend/Adapter/ZendTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/Frontend/Adapter/ZendTest.php index 4ed199f9c62fc..fb646c7f87622 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/Frontend/Adapter/ZendTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/Frontend/Adapter/ZendTest.php @@ -17,7 +17,10 @@ class ZendTest extends \PHPUnit\Framework\TestCase public function testProxyMethod($method, $params, $expectedParams, $expectedResult) { $frontendMock = $this->createMock(\Zend_Cache_Core::class); - $object = new \Magento\Framework\Cache\Frontend\Adapter\Zend($frontendMock); + $frontendFactory = function () use ($frontendMock) { + return $frontendMock; + }; + $object = new \Magento\Framework\Cache\Frontend\Adapter\Zend($frontendFactory); $helper = new \Magento\Framework\TestFramework\Unit\Helper\ProxyTesting(); $result = $helper->invokeWithExpectations( $object, @@ -82,7 +85,11 @@ public function testCleanException($cleaningMode, $expectedErrorMessage) { $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage($expectedErrorMessage); - $object = new \Magento\Framework\Cache\Frontend\Adapter\Zend($this->createMock(\Zend_Cache_Core::class)); + $frontendMock = $this->createMock(\Zend_Cache_Core::class); + $frontendFactory = function () use ($frontendMock) { + return $frontendMock; + }; + $object = new \Magento\Framework\Cache\Frontend\Adapter\Zend($frontendFactory); $object->clean($cleaningMode); } @@ -110,7 +117,10 @@ public function cleanExceptionDataProvider() public function testGetLowLevelFrontend() { $frontendMock = $this->createMock(\Zend_Cache_Core::class); - $object = new \Magento\Framework\Cache\Frontend\Adapter\Zend($frontendMock); + $frontendFactory = function () use ($frontendMock) { + return $frontendMock; + }; + $object = new \Magento\Framework\Cache\Frontend\Adapter\Zend($frontendFactory); $this->assertSame($frontendMock, $object->getLowLevelFrontend()); } } diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/Frontend/Decorator/ProfilerTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/Frontend/Decorator/ProfilerTest.php index bef4617add1b9..34e50900bc64e 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/Frontend/Decorator/ProfilerTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/Frontend/Decorator/ProfilerTest.php @@ -63,7 +63,10 @@ public function proxyMethodDataProvider() { $backend = new \Zend_Cache_Backend_BlackHole(); $adaptee = $this->createMock(\Zend_Cache_Core::class); - $lowLevelFrontend = new \Magento\Framework\Cache\Frontend\Adapter\Zend($adaptee); + $frontendFactory = function () use ($adaptee) { + return $adaptee; + }; + $lowLevelFrontend = new \Magento\Framework\Cache\Frontend\Adapter\Zend($frontendFactory); return [ [ diff --git a/lib/internal/Magento/Framework/Code/Generator.php b/lib/internal/Magento/Framework/Code/Generator.php index a845712b46598..4dec7d1a28146 100644 --- a/lib/internal/Magento/Framework/Code/Generator.php +++ b/lib/internal/Magento/Framework/Code/Generator.php @@ -7,6 +7,11 @@ use Magento\Framework\Code\Generator\DefinedClasses; use Magento\Framework\Code\Generator\EntityAbstract; +use Magento\Framework\Code\Generator\Io; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Phrase; +use Magento\Framework\Filesystem\Driver\File; +use Psr\Log\LoggerInterface; class Generator { @@ -17,7 +22,7 @@ class Generator const GENERATION_SKIP = 'skip'; /** - * @var \Magento\Framework\Code\Generator\Io + * @var Io */ protected $_ioObject; @@ -32,26 +37,33 @@ class Generator protected $definedClasses; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $objectManager; /** - * @param Generator\Io $ioObject - * @param array $generatedEntities + * Logger instance + * + * @var LoggerInterface + */ + private $logger; + + /** + * @param Generator\Io $ioObject + * @param array $generatedEntities * @param DefinedClasses $definedClasses + * @param LoggerInterface|null $logger */ public function __construct( - \Magento\Framework\Code\Generator\Io $ioObject = null, + Io $ioObject = null, array $generatedEntities = [], - DefinedClasses $definedClasses = null + DefinedClasses $definedClasses = null, + LoggerInterface $logger = null ) { - $this->_ioObject = $ioObject - ?: new \Magento\Framework\Code\Generator\Io( - new \Magento\Framework\Filesystem\Driver\File() - ); + $this->_ioObject = $ioObject ?: new Io(new File()); $this->definedClasses = $definedClasses ?: new DefinedClasses(); $this->_generatedEntities = $generatedEntities; + $this->logger = $logger; } /** @@ -111,8 +123,16 @@ public function generateClass($className) if ($generator !== null) { $this->tryToLoadSourceClass($className, $generator); if (!($file = $generator->generate())) { + /** @var $logger LoggerInterface */ $errors = $generator->getErrors(); - throw new \RuntimeException(implode(' ', $errors) . ' in [' . $className . ']'); + $errors[] = 'Class ' . $className . ' generation error: The requested class did not generate properly, ' + . 'because the \'generated\' directory permission is read-only. ' + . 'If --- after running the \'bin/magento setup:di:compile\' CLI command when the \'generated\' ' + . 'directory permission is set to write --- the requested class did not generate properly, then ' + . 'you must add the generated class object to the signature of the related construct method, only.'; + $message = implode(PHP_EOL, $errors); + $this->getLogger()->critical($message); + throw new \RuntimeException($message); } if (!$this->definedClasses->isClassLoadableFromMemory($className)) { $this->_ioObject->includeFile($file); @@ -121,13 +141,26 @@ public function generateClass($className) } } + /** + * Retrieve logger + * + * @return LoggerInterface + */ + private function getLogger() + { + if (!$this->logger) { + $this->logger = $this->getObjectManager()->get(LoggerInterface::class); + } + return $this->logger; + } + /** * Create entity generator * * @param string $generatorClass * @param string $entityName * @param string $className - * @return \Magento\Framework\Code\Generator\EntityAbstract + * @return EntityAbstract */ protected function createGeneratorInstance($generatorClass, $entityName, $className) { @@ -140,10 +173,10 @@ protected function createGeneratorInstance($generatorClass, $entityName, $classN /** * Set object manager instance. * - * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param ObjectManagerInterface $objectManager * @return $this */ - public function setObjectManager(\Magento\Framework\ObjectManagerInterface $objectManager) + public function setObjectManager(ObjectManagerInterface $objectManager) { $this->objectManager = $objectManager; return $this; @@ -152,11 +185,11 @@ public function setObjectManager(\Magento\Framework\ObjectManagerInterface $obje /** * Get object manager instance. * - * @return \Magento\Framework\ObjectManagerInterface + * @return ObjectManagerInterface */ public function getObjectManager() { - if (!($this->objectManager instanceof \Magento\Framework\ObjectManagerInterface)) { + if (!($this->objectManager instanceof ObjectManagerInterface)) { throw new \LogicException( "Object manager was expected to be set using setObjectManger() " . "before getObjectManager() invocation." @@ -169,7 +202,7 @@ public function getObjectManager() * Try to load/generate source class to check if it is valid or not. * * @param string $className - * @param \Magento\Framework\Code\Generator\EntityAbstract $generator + * @param EntityAbstract $generator * @return void * @throws \RuntimeException */ @@ -178,7 +211,7 @@ protected function tryToLoadSourceClass($className, $generator) $sourceClassName = $generator->getSourceClassName(); if (!$this->definedClasses->isClassLoadable($sourceClassName)) { if ($this->generateClass($sourceClassName) !== self::GENERATION_SUCCESS) { - $phrase = new \Magento\Framework\Phrase( + $phrase = new Phrase( 'Source class "%1" for "%2" generation does not exist.', [$sourceClassName, $className] ); diff --git a/lib/internal/Magento/Framework/Code/Test/Unit/GeneratorTest.php b/lib/internal/Magento/Framework/Code/Test/Unit/GeneratorTest.php index bd58e7d0e8a2f..9cc93f7620b1f 100644 --- a/lib/internal/Magento/Framework/Code/Test/Unit/GeneratorTest.php +++ b/lib/internal/Magento/Framework/Code/Test/Unit/GeneratorTest.php @@ -3,14 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\Code\Test\Unit; use Magento\Framework\Code\Generator; use Magento\Framework\Code\Generator\DefinedClasses; use Magento\Framework\Code\Generator\Io; - -class GeneratorTest extends \PHPUnit\Framework\TestCase +use Psr\Log\LoggerInterface; +use Magento\Framework\ObjectManager\Code\Generator\Factory; +use Magento\Framework\ObjectManager\Code\Generator\Proxy; +use Magento\Framework\Interception\Code\Generator\Interceptor; +use PHPUnit_Framework_MockObject_MockObject as Mock; +use PHPUnit\Framework\TestCase; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Code\Generator\EntityAbstract; +use Magento\GeneratedClass\Factory as GeneratedClassFactory; + +class GeneratorTest extends TestCase { /** * Class name parameter value @@ -22,45 +30,57 @@ class GeneratorTest extends \PHPUnit\Framework\TestCase * * @var array */ - protected $expectedEntities = [ - 'factory' => \Magento\Framework\ObjectManager\Code\Generator\Factory::ENTITY_TYPE, - 'proxy' => \Magento\Framework\ObjectManager\Code\Generator\Proxy::ENTITY_TYPE, - 'interceptor' => \Magento\Framework\Interception\Code\Generator\Interceptor::ENTITY_TYPE, + private $expectedEntities = [ + 'factory' => Factory::ENTITY_TYPE, + 'proxy' => Proxy::ENTITY_TYPE, + 'interceptor' => Interceptor::ENTITY_TYPE, ]; /** * System under test * - * @var \Magento\Framework\Code\Generator + * @var Generator */ - protected $model; + private $model; - /** @var \PHPUnit_Framework_MockObject_MockObject|Io */ - protected $ioObjectMock; + /** + * @var Io|Mock + */ + private $ioObjectMock; - /** @var \Magento\Framework\Code\Generator\DefinedClasses | \PHPUnit_Framework_MockObject_MockObject */ - protected $definedClassesMock; + /** + * @var DefinedClasses|Mock + */ + private $definedClassesMock; + + /** + * @var LoggerInterface|Mock + */ + private $loggerMock; protected function setUp() { - $this->definedClassesMock = $this->createMock(\Magento\Framework\Code\Generator\DefinedClasses::class); - $this->ioObjectMock = $this->getMockBuilder(\Magento\Framework\Code\Generator\Io::class) + $this->definedClassesMock = $this->createMock(DefinedClasses::class); + $this->ioObjectMock = $this->getMockBuilder(Io::class) ->disableOriginalConstructor() ->getMock(); - $this->model = $this->buildModel( + $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + + $this->model = new Generator( $this->ioObjectMock, [ - 'factory' => \Magento\Framework\ObjectManager\Code\Generator\Factory::class, - 'proxy' => \Magento\Framework\ObjectManager\Code\Generator\Proxy::class, - 'interceptor' => \Magento\Framework\Interception\Code\Generator\Interceptor::class + 'factory' => Factory::class, + 'proxy' => Proxy::class, + 'interceptor' => Interceptor::class, ], - $this->definedClassesMock + $this->definedClassesMock, + $this->loggerMock ); } public function testGetGeneratedEntities() { - $this->model = $this->buildModel( + $this->model = new Generator( $this->ioObjectMock, ['factory', 'proxy', 'interceptor'], $this->definedClassesMock @@ -69,14 +89,16 @@ public function testGetGeneratedEntities() } /** + * @param string $className + * @param string $entityType * @expectedException \RuntimeException * @dataProvider generateValidClassDataProvider */ public function testGenerateClass($className, $entityType) { - $objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); $fullClassName = $className . $entityType; - $entityGeneratorMock = $this->getMockBuilder(\Magento\Framework\Code\Generator\EntityAbstract::class) + $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) ->disableOriginalConstructor() ->getMock(); $objectManagerMock->expects($this->once())->method('create')->willReturn($entityGeneratorMock); @@ -87,7 +109,7 @@ public function testGenerateClass($className, $entityType) public function testGenerateClassWithWrongName() { $this->assertEquals( - \Magento\Framework\Code\Generator::GENERATION_ERROR, + Generator::GENERATION_ERROR, $this->model->generateClass(self::SOURCE_CLASS) ); } @@ -95,12 +117,12 @@ public function testGenerateClassWithWrongName() /** * @expectedException \RuntimeException */ - public function testGenerateClassWithError() + public function testGenerateClassWhenClassIsNotGenerationSuccess() { $expectedEntities = array_values($this->expectedEntities); $resultClassName = self::SOURCE_CLASS . ucfirst(array_shift($expectedEntities)); - $objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); - $entityGeneratorMock = $this->getMockBuilder(\Magento\Framework\Code\Generator\EntityAbstract::class) + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); + $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) ->disableOriginalConstructor() ->getMock(); $objectManagerMock->expects($this->once())->method('create')->willReturn($entityGeneratorMock); @@ -108,6 +130,57 @@ public function testGenerateClassWithError() $this->model->generateClass($resultClassName); } + /** + * @inheritdoc + */ + public function testGenerateClassWithErrors() + { + $expectedEntities = array_values($this->expectedEntities); + $resultClassName = self::SOURCE_CLASS . ucfirst(array_shift($expectedEntities)); + $errorMessages = [ + 'Error message 0', + 'Error message 1', + 'Error message 2', + ]; + $mainErrorMessage = 'Class ' . $resultClassName . ' generation error: The requested class did not generate ' + . 'properly, because the \'generated\' directory permission is read-only. ' + . 'If --- after running the \'bin/magento setup:di:compile\' CLI command when the \'generated\' ' + . 'directory permission is set to write --- the requested class did not generate properly, then ' + . 'you must add the generated class object to the signature of the related construct method, only.'; + $FinalErrorMessage = implode(PHP_EOL, $errorMessages) . "\n" . $mainErrorMessage; + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage($FinalErrorMessage); + + /** @var ObjectManagerInterface|Mock $objectManagerMock */ + $objectManagerMock = $this->createMock(ObjectManagerInterface::class); + /** @var EntityAbstract|Mock $entityGeneratorMock */ + $entityGeneratorMock = $this->getMockBuilder(EntityAbstract::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManagerMock->expects($this->once()) + ->method('create') + ->willReturn($entityGeneratorMock); + $entityGeneratorMock->expects($this->once()) + ->method('getSourceClassName') + ->willReturn(self::SOURCE_CLASS); + $this->definedClassesMock->expects($this->once()) + ->method('isClassLoadable') + ->with(self::SOURCE_CLASS) + ->willReturn(true); + $entityGeneratorMock->expects($this->once()) + ->method('generate') + ->willReturn(false); + $entityGeneratorMock->expects($this->once()) + ->method('getErrors') + ->willReturn($errorMessages); + $this->loggerMock->expects($this->once()) + ->method('critical') + ->with($FinalErrorMessage); + $this->model->setObjectManager($objectManagerMock); + $this->model->generateClass($resultClassName); + } + /** * @dataProvider trueFalseDataProvider */ @@ -124,8 +197,8 @@ public function testGenerateClassWithExistName($fileExists) $this->ioObjectMock->expects($this->exactly($includeFileInvokeCount))->method('includeFile'); $this->assertEquals( - \Magento\Framework\Code\Generator::GENERATION_SKIP, - $this->model->generateClass(\Magento\GeneratedClass\Factory::class) + Generator::GENERATION_SKIP, + $this->model->generateClass(GeneratedClassFactory::class) ); } @@ -154,17 +227,4 @@ public function generateValidClassDataProvider() } return $data; } - - /** - * Build SUT object - * - * @param Io $ioObject - * @param array $generatedEntities - * @param DefinedClasses $definedClasses - * @return Generator - */ - private function buildModel(Io $ioObject, array $generatedEntities, DefinedClasses $definedClasses) - { - return new Generator($ioObject, $generatedEntities, $definedClasses); - } } diff --git a/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php b/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php index 801d99373bc50..15ceb0c6c2755 100644 --- a/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php +++ b/lib/internal/Magento/Framework/Composer/Test/Unit/DependencyCheckerTest.php @@ -69,7 +69,7 @@ function ($input, $buffer) { $buffer->writeln($output); } ); - $composerApp->Expects($this->at(6))->method('run')->willReturnCallback( + $composerApp->expects($this->at(6))->method('run')->willReturnCallback( function ($input, $buffer) { $output = 'magento/package-d requires magento/package-c (1.0)' . PHP_EOL . 'magento/project-community-edition requires magento/package-a (1.0)' . PHP_EOL; diff --git a/lib/internal/Magento/Framework/Data/Form/FormKey/Validator.php b/lib/internal/Magento/Framework/Data/Form/FormKey/Validator.php index 0dbc9c879462e..225ff1fd140a9 100644 --- a/lib/internal/Magento/Framework/Data/Form/FormKey/Validator.php +++ b/lib/internal/Magento/Framework/Data/Form/FormKey/Validator.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Data\Form\FormKey; +use Magento\Framework\Encryption\Helper\Security; + /** * @api */ @@ -32,9 +34,7 @@ public function __construct(\Magento\Framework\Data\Form\FormKey $formKey) public function validate(\Magento\Framework\App\RequestInterface $request) { $formKey = $request->getParam('form_key', null); - if (!$formKey || $formKey !== $this->_formKey->getFormKey()) { - return false; - } - return true; + + return $formKey && Security::compareStrings($formKey, $this->_formKey->getFormKey()); } } diff --git a/lib/internal/Magento/Framework/File/Size.php b/lib/internal/Magento/Framework/File/Size.php index 9817b3b746303..c5a51ec1760e7 100644 --- a/lib/internal/Magento/Framework/File/Size.php +++ b/lib/internal/Magento/Framework/File/Size.php @@ -36,7 +36,7 @@ class Size */ public function getPostMaxSize() { - return $this->_iniget('post_max_size'); + return $this->_iniGet('post_max_size'); } /** @@ -46,7 +46,7 @@ public function getPostMaxSize() */ public function getUploadMaxSize() { - return $this->_iniget('upload_max_filesize'); + return $this->_iniGet('upload_max_filesize'); } /** diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/PathValidator.php b/lib/internal/Magento/Framework/Filesystem/Directory/PathValidator.php new file mode 100644 index 0000000000000..fe0e6b37666b7 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Directory/PathValidator.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Directory; + +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Phrase; + +/** + * @inheritDoc + * + * Validates paths using driver. + */ +class PathValidator implements PathValidatorInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @param DriverInterface $driver + */ + public function __construct(DriverInterface $driver) + { + $this->driver = $driver; + } + + /** + * @inheritDoc + */ + public function validate( + string $directoryPath, + string $path, + ?string $scheme = null, + bool $absolutePath = false + ): void { + $realDirectoryPath = $this->driver->getRealPathSafety($directoryPath); + if ($realDirectoryPath[-1] !== DIRECTORY_SEPARATOR) { + $realDirectoryPath .= DIRECTORY_SEPARATOR; + } + if (!$absolutePath) { + $actualPath = $this->driver->getRealPathSafety( + $this->driver->getAbsolutePath( + $realDirectoryPath, + $path, + $scheme + ) + ); + } else { + $actualPath = $this->driver->getRealPathSafety($path); + } + + if (mb_strpos($actualPath, $realDirectoryPath) !== 0 + && $path .DIRECTORY_SEPARATOR !== $realDirectoryPath + ) { + throw new ValidatorException( + new Phrase( + 'Path "%1" cannot be used with directory "%2"', + [$path, $directoryPath] + ) + ); + } + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/PathValidatorInterface.php b/lib/internal/Magento/Framework/Filesystem/Directory/PathValidatorInterface.php new file mode 100644 index 0000000000000..ecb7da9aaeab6 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Directory/PathValidatorInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Directory; + +use Magento\Framework\Exception\ValidatorException; + +/** + * Validate paths to be used with directories. + */ +interface PathValidatorInterface +{ + /** + * Validate if path can be used with a directory. + * + * @param string $directoryPath + * @param string $path + * @param string|null $scheme + * @param bool $absolutePath Is given path an absolute path?. + * @throws ValidatorException + * + * @return void + */ + public function validate( + string $directoryPath, + string $path, + ?string $scheme = null, + bool $absolutePath = false + ): void; +} diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Read.php b/lib/internal/Magento/Framework/Filesystem/Directory/Read.php index 18c93f2f4e1c8..a3a4cec59953f 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Read.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Read.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\ValidatorException; /** * @api @@ -33,21 +34,52 @@ class Read implements ReadInterface */ protected $driver; + /** + * @var PathValidatorInterface|null + */ + private $pathValidator; + /** * Constructor. Set properties. * * @param \Magento\Framework\Filesystem\File\ReadFactory $fileFactory * @param \Magento\Framework\Filesystem\DriverInterface $driver * @param string $path + * @param PathValidatorInterface|null $pathValidator */ public function __construct( \Magento\Framework\Filesystem\File\ReadFactory $fileFactory, \Magento\Framework\Filesystem\DriverInterface $driver, - $path + $path, + ?PathValidatorInterface $pathValidator = null ) { $this->fileFactory = $fileFactory; $this->driver = $driver; $this->setPath($path); + $this->pathValidator = $pathValidator; + } + + /** + * @param null|string $path + * @param null|string $scheme + * @param bool $absolutePath + * @throws ValidatorException + * + * @return void + */ + protected function validatePath( + ?string $path, + ?string $scheme = null, + bool $absolutePath = false + ): void { + if ($path && $this->pathValidator) { + $this->pathValidator->validate( + $this->path, + $path, + $scheme, + $absolutePath + ); + } } /** @@ -69,10 +101,13 @@ protected function setPath($path) * * @param string $path * @param string $scheme + * @throws ValidatorException * @return string */ public function getAbsolutePath($path = null, $scheme = null) { + $this->validatePath($path, $scheme); + return $this->driver->getAbsolutePath($this->path, $path, $scheme); } @@ -80,10 +115,17 @@ public function getAbsolutePath($path = null, $scheme = null) * Retrieves relative path * * @param string $path + * @throws ValidatorException * @return string */ public function getRelativePath($path = null) { + $this->validatePath( + $path, + null, + $path && $path[0] === DIRECTORY_SEPARATOR + ); + return $this->driver->getRelativePath($this->path, $path); } @@ -91,10 +133,13 @@ public function getRelativePath($path = null) * Retrieve list of all entities in given path * * @param string|null $path + * @throws ValidatorException * @return string[] */ public function read($path = null) { + $this->validatePath($path); + $files = $this->driver->readDirectory($this->driver->getAbsolutePath($this->path, $path)); $result = []; foreach ($files as $file) { @@ -107,10 +152,13 @@ public function read($path = null) * Read recursively * * @param null $path + * @throws ValidatorException * @return string[] */ public function readRecursively($path = null) { + $this->validatePath($path); + $result = []; $paths = $this->driver->readDirectoryRecursively($this->driver->getAbsolutePath($this->path, $path)); /** @var \FilesystemIterator $file */ @@ -126,10 +174,13 @@ public function readRecursively($path = null) * * @param string $pattern * @param string $path [optional] + * @throws ValidatorException * @return string[] */ public function search($pattern, $path = null) { + $this->validatePath($path); + if ($path) { $absolutePath = $this->driver->getAbsolutePath($this->path, $this->getRelativePath($path)); } else { @@ -150,9 +201,12 @@ public function search($pattern, $path = null) * @param string $path [optional] * @return bool * @throws \Magento\Framework\Exception\FileSystemException + * @throws ValidatorException */ public function isExist($path = null) { + $this->validatePath($path); + return $this->driver->isExists($this->driver->getAbsolutePath($this->path, $path)); } @@ -162,9 +216,12 @@ public function isExist($path = null) * @param string $path * @return array * @throws \Magento\Framework\Exception\FileSystemException + * @throws ValidatorException */ public function stat($path) { + $this->validatePath($path); + return $this->driver->stat($this->driver->getAbsolutePath($this->path, $path)); } @@ -174,9 +231,12 @@ public function stat($path) * @param string $path [optional] * @return bool * @throws \Magento\Framework\Exception\FileSystemException + * @throws ValidatorException */ public function isReadable($path = null) { + $this->validatePath($path); + return $this->driver->isReadable($this->driver->getAbsolutePath($this->path, $path)); } @@ -184,11 +244,14 @@ public function isReadable($path = null) * Open file in read mode * * @param string $path + * @throws ValidatorException * * @return \Magento\Framework\Filesystem\File\ReadInterface */ public function openFile($path) { + $this->validatePath($path); + return $this->fileFactory->create( $this->driver->getAbsolutePath($this->path, $path), $this->driver @@ -203,9 +266,12 @@ public function openFile($path) * @param resource|null $context * @return string * @throws FileSystemException + * @throws ValidatorException */ public function readFile($path, $flag = null, $context = null) { + $this->validatePath($path); + $absolutePath = $this->driver->getAbsolutePath($this->path, $path); return $this->driver->fileGetContents($absolutePath, $flag, $context); } @@ -214,10 +280,13 @@ public function readFile($path, $flag = null, $context = null) * Check whether given path is file * * @param string $path + * @throws ValidatorException * @return bool */ public function isFile($path) { + $this->validatePath($path); + return $this->driver->isFile($this->driver->getAbsolutePath($this->path, $path)); } @@ -225,10 +294,13 @@ public function isFile($path) * Check whether given path is directory * * @param string $path [optional] + * @throws ValidatorException * @return bool */ public function isDirectory($path = null) { + $this->validatePath($path); + return $this->driver->isDirectory($this->driver->getAbsolutePath($this->path, $path)); } } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php index a9fe7dcb7bf06..25a290455dc46 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php @@ -36,7 +36,15 @@ public function __construct(DriverPool $driverPool) public function create($path, $driverCode = DriverPool::FILE) { $driver = $this->driverPool->getDriver($driverCode); - $factory = new \Magento\Framework\Filesystem\File\ReadFactory($this->driverPool); - return new Read($factory, $driver, $path); + $factory = new \Magento\Framework\Filesystem\File\ReadFactory( + $this->driverPool + ); + + return new Read( + $factory, + $driver, + $path, + new PathValidator($driver) + ); } } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 900d8423b31d8..78732d73456f7 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -7,6 +7,7 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\ValidatorException; class Write extends Read implements WriteInterface { @@ -24,16 +25,16 @@ class Write extends Read implements WriteInterface * @param \Magento\Framework\Filesystem\DriverInterface $driver * @param string $path * @param int $createPermissions + * @param PathValidatorInterface|null $pathValidator */ public function __construct( \Magento\Framework\Filesystem\File\WriteFactory $fileFactory, \Magento\Framework\Filesystem\DriverInterface $driver, $path, - $createPermissions = null + $createPermissions = null, + ?PathValidatorInterface $pathValidator = null ) { - $this->fileFactory = $fileFactory; - $this->driver = $driver; - $this->setPath($path); + parent::__construct($fileFactory, $driver, $path, $pathValidator); if (null !== $createPermissions) { $this->permissions = $createPermissions; } @@ -80,9 +81,11 @@ protected function assertIsFile($path) * @param string $path * @return bool * @throws FileSystemException + * @throws ValidatorException */ public function create($path = null) { + $this->validatePath($path); $absolutePath = $this->driver->getAbsolutePath($this->path, $path); if ($this->driver->isDirectory($absolutePath)) { return true; @@ -98,9 +101,11 @@ public function create($path = null) * @param WriteInterface $targetDirectory * @return bool * @throws FileSystemException + * @throws ValidatorException */ public function renameFile($path, $newPath, WriteInterface $targetDirectory = null) { + $this->validatePath($path); $this->assertIsFile($path); $targetDirectory = $targetDirectory ?: $this; if (!$targetDirectory->isExist($this->driver->getParentDirectory($newPath))) { @@ -119,9 +124,11 @@ public function renameFile($path, $newPath, WriteInterface $targetDirectory = nu * @param WriteInterface $targetDirectory * @return bool * @throws FileSystemException + * @throws ValidatorException */ public function copyFile($path, $destination, WriteInterface $targetDirectory = null) { + $this->validatePath($path); $this->assertIsFile($path); $targetDirectory = $targetDirectory ?: $this; @@ -142,9 +149,11 @@ public function copyFile($path, $destination, WriteInterface $targetDirectory = * @param WriteInterface $targetDirectory [optional] * @return bool * @throws \Magento\Framework\Exception\FileSystemException + * @throws ValidatorException */ public function createSymlink($path, $destination, WriteInterface $targetDirectory = null) { + $this->validatePath($path); $targetDirectory = $targetDirectory ?: $this; $parentDirectory = $this->driver->getParentDirectory($destination); if (!$targetDirectory->isExist($parentDirectory)) { @@ -162,9 +171,11 @@ public function createSymlink($path, $destination, WriteInterface $targetDirecto * @param string $path * @return bool * @throws FileSystemException + * @throws ValidatorException */ public function delete($path = null) { + $this->validatePath($path); if (!$this->isExist($path)) { return true; } @@ -184,10 +195,13 @@ public function delete($path = null) * @param int $permissions * @return bool * @throws FileSystemException + * @throws ValidatorException */ public function changePermissions($path, $permissions) { + $this->validatePath($path); $absolutePath = $this->driver->getAbsolutePath($this->path, $path); + return $this->driver->changePermissions($absolutePath, $permissions); } @@ -199,10 +213,13 @@ public function changePermissions($path, $permissions) * @param int $filePermissions * @return bool * @throws FileSystemException + * @throws ValidatorException */ public function changePermissionsRecursively($path, $dirPermissions, $filePermissions) { + $this->validatePath($path); $absolutePath = $this->driver->getAbsolutePath($this->path, $path); + return $this->driver->changePermissionsRecursively($absolutePath, $dirPermissions, $filePermissions); } @@ -213,9 +230,12 @@ public function changePermissionsRecursively($path, $dirPermissions, $filePermis * @param int|null $modificationTime * @return bool * @throws FileSystemException + * @throws ValidatorException */ public function touch($path, $modificationTime = null) { + $this->validatePath($path); + $folder = $this->driver->getParentDirectory($path); $this->create($folder); $this->assertWritable($folder); @@ -228,9 +248,12 @@ public function touch($path, $modificationTime = null) * @param null $path * @return bool * @throws \Magento\Framework\Exception\FileSystemException + * @throws ValidatorException */ public function isWritable($path = null) { + $this->validatePath($path); + return $this->driver->isWritable($this->driver->getAbsolutePath($this->path, $path)); } @@ -241,13 +264,16 @@ public function isWritable($path = null) * @param string $mode * @return \Magento\Framework\Filesystem\File\WriteInterface * @throws \Magento\Framework\Exception\FileSystemException + * @throws ValidatorException */ public function openFile($path, $mode = 'w') { + $this->validatePath($path); $folder = dirname($path); $this->create($folder); $this->assertWritable($this->isExist($path) ? $path : $folder); $absolutePath = $this->driver->getAbsolutePath($this->path, $path); + return $this->fileFactory->create($absolutePath, $this->driver, $mode); } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php index a723ed6a7bea6..ff14b12f62047 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php @@ -37,7 +37,16 @@ public function __construct(DriverPool $driverPool) public function create($path, $driverCode = DriverPool::FILE, $createPermissions = null) { $driver = $this->driverPool->getDriver($driverCode); - $factory = new \Magento\Framework\Filesystem\File\WriteFactory($this->driverPool); - return new Write($factory, $driver, $path, $createPermissions); + $factory = new \Magento\Framework\Filesystem\File\WriteFactory( + $this->driverPool + ); + + return new Write( + $factory, + $driver, + $path, + $createPermissions, + new PathValidator($driver) + ); } } diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 69382d66e349e..b54b02bd6de98 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -950,6 +950,13 @@ public function getRealPathSafety($path) if (strpos($path, DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR) === false) { return $path; } + + //Removing redundant directory separators. + $path = preg_replace( + '/\\' .DIRECTORY_SEPARATOR .'\\' .DIRECTORY_SEPARATOR .'+/', + DIRECTORY_SEPARATOR, + $path + ); $pathParts = explode(DIRECTORY_SEPARATOR, $path); $realPath = []; foreach ($pathParts as $pathPart) { diff --git a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php index 64683a104784d..a45d6a62488f6 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php @@ -8,7 +8,7 @@ use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; -class WriteFactory +class WriteFactory extends ReadFactory { /** * Pool of filesystem drivers @@ -24,6 +24,7 @@ class WriteFactory */ public function __construct(DriverPool $driverPool) { + parent::__construct($driverPool); $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filter/Factory.php b/lib/internal/Magento/Framework/Filter/Factory.php index a5e5978a83f06..dbe037ecbbbd1 100644 --- a/lib/internal/Magento/Framework/Filter/Factory.php +++ b/lib/internal/Magento/Framework/Filter/Factory.php @@ -32,6 +32,7 @@ class Factory extends AbstractFactory 'decrypt' => \Magento\Framework\Filter\Decrypt::class, 'translit' => \Magento\Framework\Filter\Translit::class, 'translitUrl' => \Magento\Framework\Filter\TranslitUrl::class, + 'truncateFilter' => \Magento\Framework\Filter\TruncateFilter::class, ]; /** diff --git a/lib/internal/Magento/Framework/Filter/FilterManager.php b/lib/internal/Magento/Framework/Filter/FilterManager.php index 52c9d017ad11c..ca5d998af833f 100644 --- a/lib/internal/Magento/Framework/Filter/FilterManager.php +++ b/lib/internal/Magento/Framework/Filter/FilterManager.php @@ -21,6 +21,7 @@ * @method string removeTags(string $value, $params = array()) * @method string stripTags(string $value, $params = array()) * @method string truncate(string $value, $params = array()) + * @method string truncateFilter(string $value, $params = array()) * @method string encrypt(string $value, $params = array()) * @method string decrypt(string $value, $params = array()) * @method string translit(string $value) diff --git a/lib/internal/Magento/Framework/Filter/Truncate.php b/lib/internal/Magento/Framework/Filter/Truncate.php index fd4fbe9910427..a4dd35b302705 100644 --- a/lib/internal/Magento/Framework/Filter/Truncate.php +++ b/lib/internal/Magento/Framework/Filter/Truncate.php @@ -10,6 +10,9 @@ * * Truncate a string to a certain length if necessary, appending the $etc string. * $remainder will contain the string that has been replaced with $etc. + * + * @deprecated + * @see \Magento\Framework\Filter\TruncateFilter */ class Truncate implements \Zend_Filter_Interface { diff --git a/lib/internal/Magento/Framework/Filter/TruncateFilter.php b/lib/internal/Magento/Framework/Filter/TruncateFilter.php new file mode 100644 index 0000000000000..a41469bb6f2a9 --- /dev/null +++ b/lib/internal/Magento/Framework/Filter/TruncateFilter.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filter; + +use Magento\Framework\Filter\TruncateFilter\Result; +use Magento\Framework\Filter\TruncateFilter\ResultFactory; + +/** + * Truncate filter + * + * Truncate a string to a certain length if necessary, appending the $etc string. + * $remainder will contain the string that has been replaced with $etc. + */ +class TruncateFilter implements \Zend_Filter_Interface +{ + /** + * @var int + */ + private $length; + + /** + * @var string + */ + private $etc; + + /** + * @var bool + */ + private $breakWords; + + /** + * @var \Magento\Framework\Stdlib\StringUtils + */ + private $stringUtils; + + /** + * @var ResultFactory + */ + private $resultFactory; + + /** + * @param \Magento\Framework\Stdlib\StringUtils $stringUtils + * @param ResultFactory $resultFactory + * @param int $length + * @param string $etc + * @param bool $breakWords + */ + public function __construct( + \Magento\Framework\Stdlib\StringUtils $stringUtils, + ResultFactory $resultFactory, + $length = 80, + $etc = '...', + $breakWords = true + ) { + $this->stringUtils = $stringUtils; + $this->resultFactory = $resultFactory; + $this->length = $length; + $this->etc = $etc; + $this->breakWords = $breakWords; + } + + /** + * Filter value + * + * @param string $string + * @return Result + */ + public function filter($string) : Result + { + /** @var Result $result */ + $result = $this->resultFactory->create(['value' => $string, 'remainder' => '']); + $length = $this->length; + if (0 == $length) { + $result->setValue(''); + return $result; + } + + $originalLength = $this->stringUtils->strlen($string); + if ($originalLength > $length) { + $length -= $this->stringUtils->strlen($this->etc); + if ($length <= 0) { + $result->setValue(''); + return $result; + } + $preparedString = $string; + $preparedLength = $length; + if (!$this->breakWords) { + $preparedString = preg_replace( + '/\s+?(\S+)?$/u', + '', + $this->stringUtils->substr($string, 0, $length + 1) + ); + $preparedLength = $this->stringUtils->strlen($preparedString); + } + $result->setRemainder($this->stringUtils->substr($string, $preparedLength, $originalLength)); + $result->setValue($this->stringUtils->substr($preparedString, 0, $length) . $this->etc); + return $result; + } + + return $result; + } +} diff --git a/lib/internal/Magento/Framework/Filter/TruncateFilter/Result.php b/lib/internal/Magento/Framework/Filter/TruncateFilter/Result.php new file mode 100644 index 0000000000000..c1ee6be6dadf5 --- /dev/null +++ b/lib/internal/Magento/Framework/Filter/TruncateFilter/Result.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filter\TruncateFilter; + +class Result +{ + /** + * @var string + */ + private $value; + + /** + * @var string + */ + private $remainder; + + /** + * Result constructor. + * @param string $value + * @param string $remainder + */ + public function __construct(string $value, string $remainder) + { + $this->value = $value; + $this->remainder = $remainder; + } + + /** + * Set result value + * + * @param string $value + * @return void + */ + public function setValue(string $value) : void + { + $this->value = $value; + } + + /** + * Get value + * + * @return string + */ + public function getValue() : string + { + return $this->value; + } + + /** + * Set remainder + * + * @param string $remainder + * @return void + */ + public function setRemainder(string $remainder) : void + { + $this->remainder = $remainder; + } + + /** + * Get remainder + * + * @return string + */ + public function getRemainder() : string + { + return $this->remainder; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php b/lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php index 668ec2bdc84e4..63fef73186b12 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/SchemaGenerator.php @@ -56,6 +56,7 @@ public function generate() : Schema $schema = $this->schemaFactory->create( [ 'query' => $this->outputMapper->getOutputType('Query'), + 'mutation' => $this->outputMapper->getOutputType('Mutation'), 'typeLoader' => function ($name) { return $this->outputMapper->getOutputType($name); }, diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index 93274b1d063bf..ea21faf3f340d 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -106,7 +106,7 @@ protected function _getImageNeedMemorySize($file) } return round( - ($imageInfo[0] * $imageInfo[1] * $imageInfo['bits'] * $imageInfo['channels'] / 8 + Pow(2, 16)) * 1.65 + ($imageInfo[0] * $imageInfo[1] * $imageInfo['bits'] * $imageInfo['channels'] / 8 + pow(2, 16)) * 1.65 ); } @@ -426,7 +426,7 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = imagecolortransparent($newWatermark, $col); imagefilledrectangle($newWatermark, 0, 0, $this->getWatermarkWidth(), $this->getWatermarkHeight(), $col); imagealphablending($newWatermark, true); - imageSaveAlpha($newWatermark, true); + imagesavealpha($newWatermark, true); imagecopyresampled( $newWatermark, $watermark, @@ -451,7 +451,7 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = imagecolortransparent($newWatermark, $col); imagefilledrectangle($newWatermark, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight, $col); imagealphablending($newWatermark, true); - imageSaveAlpha($newWatermark, true); + imagesavealpha($newWatermark, true); imagecopyresampled( $newWatermark, $watermark, @@ -669,7 +669,7 @@ private function imageDestroy() private function _saveAlpha($imageHandler) { $background = imagecolorallocate($imageHandler, 0, 0, 0); - ImageColorTransparent($imageHandler, $background); + imagecolortransparent($imageHandler, $background); imagealphablending($imageHandler, false); imagesavealpha($imageHandler, true); } diff --git a/lib/internal/Magento/Framework/Indexer/Action/Base.php b/lib/internal/Magento/Framework/Indexer/Action/Base.php index 11cf1cec62636..636335192cbc5 100644 --- a/lib/internal/Magento/Framework/Indexer/Action/Base.php +++ b/lib/internal/Magento/Framework/Indexer/Action/Base.php @@ -221,7 +221,7 @@ protected function prepareDataSource(array $ids = []) { return !count($ids) ? $this->createResultCollection() - : $this->createResultCollection()->addFieldToFilter($this->getPrimaryResource()->getIdFieldname(), $ids); + : $this->createResultCollection()->addFieldToFilter($this->getPrimaryResource()->getIdFieldName(), $ids); } /** diff --git a/lib/internal/Magento/Framework/Locale/Format.php b/lib/internal/Magento/Framework/Locale/Format.php index 00379c87daaf9..89f6957011876 100644 --- a/lib/internal/Magento/Framework/Locale/Format.php +++ b/lib/internal/Magento/Framework/Locale/Format.php @@ -131,7 +131,6 @@ public function getPriceFormat($localeCode = null, $currencyCode = null) } else { $group = strrpos($format, '.'); } - $integerRequired = strpos($format, '.') - strpos($format, '0'); $result = [ //TODO: change interface @@ -141,7 +140,7 @@ public function getPriceFormat($localeCode = null, $currencyCode = null) 'decimalSymbol' => $decimalSymbol, 'groupSymbol' => $groupSymbol, 'groupLength' => $group, - 'integerRequired' => $integerRequired, + 'integerRequired' => $totalPrecision == 0, ]; return $result; diff --git a/lib/internal/Magento/Framework/Math/FloatComparator.php b/lib/internal/Magento/Framework/Math/FloatComparator.php new file mode 100644 index 0000000000000..4053404369956 --- /dev/null +++ b/lib/internal/Magento/Framework/Math/FloatComparator.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Math; + +/** + * Contains methods to compare float digits. + * + * @api + */ +class FloatComparator +{ + /** + * Precision for floats comparing. + * + * @var float + */ + private static $epsilon = 0.00001; + + /** + * Compares two float digits. + * + * @param float $a + * @param float $b + * @return bool + */ + public function equal(float $a, float $b): bool + { + return abs($a - $b) <= self::$epsilon; + } + + /** + * Compares if the first argument greater than the second argument. + * + * @param float $a + * @param float $b + * @return bool + */ + public function greaterThan(float $a, float $b): bool + { + return ($a - $b) > self::$epsilon; + } + + /** + * Compares if the first argument greater or equal to the second. + * + * @param float $a + * @param float $b + * @return bool + */ + public function greaterThanOrEqual(float $a, float $b): bool + { + return $this->equal($a, $b) || $this->greaterThan($a, $b); + } +} diff --git a/lib/internal/Magento/Framework/Math/Test/Unit/FloatComparatorTest.php b/lib/internal/Magento/Framework/Math/Test/Unit/FloatComparatorTest.php new file mode 100644 index 0000000000000..c9e5143ee789e --- /dev/null +++ b/lib/internal/Magento/Framework/Math/Test/Unit/FloatComparatorTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Math\Test\Unit; + +use Magento\Framework\Math\FloatComparator; +use PHPUnit\Framework\TestCase; + +class FloatComparatorTest extends TestCase +{ + /** + * @var FloatComparator + */ + private $comparator; + + /** + * @inheritdoc + */ + protected function setUp() + { + $this->comparator = new FloatComparator(); + } + + /** + * Checks a case when `a` and `b` are equal. + * + * @param float $a + * @param float $b + * @param bool $expected + * @dataProvider eqDataProvider + */ + public function testEq(float $a, float $b, bool $expected) + { + self::assertEquals($expected, $this->comparator->equal($a, $b)); + } + + /** + * Gets list of variations to compare equal float. + * + * @return array + */ + public function eqDataProvider(): array + { + return [ + [10, 10.00001, true], + [10, 10.000001, true], + [10.0000099, 10.00001, true], + [1, 1.0001, false], + [1, -1.00001, false], + ]; + } + + /** + * Checks a case when `a` > `b`. + * + * @param float $a + * @param float $b + * @param bool $expected + * @dataProvider gtDataProvider + */ + public function testGt(float $a, float $b, bool $expected) + { + self::assertEquals($expected, $this->comparator->greaterThan($a, $b)); + } + + /** + * Gets list of variations to compare if `a` > `b`. + * + * @return array + */ + public function gtDataProvider(): array + { + return [ + [10, 10.00001, false], + [10, 10.000001, false], + [10.0000099, 10.00001, false], + [1.0001, 1, true], + [1, -1.00001, true], + ]; + } + + /** + * Checks a case when `a` >= `b`. + * + * @param float $a + * @param float $b + * @param bool $expected + * @dataProvider gteDataProvider + */ + public function testGte(float $a, float $b, bool $expected) + { + self::assertEquals($expected, $this->comparator->greaterThanOrEqual($a, $b)); + } + + /** + * Gets list of variations to compare if `a` >= `b`. + * + * @return array + */ + public function gteDataProvider(): array + { + return [ + [10, 10.00001, true], + [10, 10.000001, true], + [10.0000099, 10.00001, true], + [1.0001, 1, true], + [1, -1.00001, true], + [1.0001, 1.001, false], + ]; + } +} diff --git a/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php b/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php index 020159985105d..15c4cb098b84d 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php +++ b/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php @@ -5,7 +5,11 @@ */ namespace Magento\Framework\ObjectManager\Factory; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Phrase; +use Psr\Log\LoggerInterface; +use Magento\Framework\App\ObjectManager; abstract class AbstractFactory implements \Magento\Framework\ObjectManager\FactoryInterface { @@ -104,11 +108,23 @@ public function getDefinitions() * @param array $args * * @return object - * + * @throws RuntimeException */ protected function createObject($type, $args) { - return new $type(...array_values($args)); + try { + return new $type(...array_values($args)); + } catch (\TypeError $exception) { + /** @var LoggerInterface $logger */ + $logger = ObjectManager::getInstance()->get(LoggerInterface::class); + $logger->critical( + sprintf('Type Error occurred when creating object: %s, %s', $type, $exception->getMessage()) + ); + + throw new RuntimeException( + new Phrase('Type Error occurred when creating object: %type', ['type' => $type]) + ); + } } /** diff --git a/lib/internal/Magento/Framework/RequireJs/Config.php b/lib/internal/Magento/Framework/RequireJs/Config.php index 08131b2cccea3..ae45e29f38911 100644 --- a/lib/internal/Magento/Framework/RequireJs/Config.php +++ b/lib/internal/Magento/Framework/RequireJs/Config.php @@ -157,7 +157,7 @@ public function getConfig() $customConfigFiles = $this->fileSource->getFiles($this->design->getDesignTheme(), self::CONFIG_FILE_NAME); foreach ($customConfigFiles as $file) { /** @var $fileReader \Magento\Framework\Filesystem\File\Read */ - $fileReader = $this->readFactory->create($file->getFileName(), DriverPool::FILE); + $fileReader = $this->readFactory->create($file->getFilename(), DriverPool::FILE); $config = $fileReader->readAll($file->getName()); $distributedConfig .= str_replace( ['%config%', '%context%'], diff --git a/app/code/Magento/Framework/Test/Unit/Setup/SchemaListenerTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php similarity index 91% rename from app/code/Magento/Framework/Test/Unit/Setup/SchemaListenerTest.php rename to lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php index 4a02cc3e0df55..4e34b3aebbf3e 100644 --- a/app/code/Magento/Framework/Test/Unit/Setup/SchemaListenerTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaListenerTest.php @@ -3,8 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace Magento\Framework\Test\Unit\Setup; +namespace Magento\Framework\Setup\Test\Unit; use Magento\Framework\DB\Ddl\Table; use Magento\Framework\Setup\SchemaListenerDefinition\BooleanDefinition; @@ -16,7 +17,7 @@ /** * Unit test for schema listener. * - * @package Magento\Framework\Test\Unit\Setup + * @package Magento\Framework\Setup\Test\Unit */ class SchemaListenerTest extends \PHPUnit\Framework\TestCase { @@ -30,7 +31,7 @@ class SchemaListenerTest extends \PHPUnit\Framework\TestCase */ private $objectManagerHelper; - protected function setUp() + protected function setUp() : void { $this->objectManagerHelper = new ObjectManagerHelper($this); $this->model = $this->objectManagerHelper->getObject( @@ -51,7 +52,7 @@ protected function setUp() /** * @return Table */ - private function getCreateTableDDL($tableName) + private function getCreateTableDDL($tableName) : Table { $table = new Table(); $table->setName($tableName); @@ -91,7 +92,7 @@ private function getCreateTableDDL($tableName) ); } - public function testRenameTable() + public function testRenameTable() : void { $this->model->setModuleName('First_Module'); $this->model->createTable($this->getCreateTableDDL('old_table')); @@ -101,7 +102,7 @@ public function testRenameTable() self::assertArrayNotHasKey('old_table', $tables['First_Module']); } - public function testDropIndex() + public function testDropIndex() : void { $this->model->setModuleName('First_Module'); $this->model->createTable($this->getCreateTableDDL('index_table')); @@ -109,7 +110,7 @@ public function testDropIndex() self::assertTrue($this->model->getTables()['First_Module']['index_table']['indexes']['INDEX_KEY']['disabled']); } - public function testCreateTable() + public function testCreateTable() : void { $this->model->setModuleName('First_Module'); $this->model->createTable($this->getCreateTableDDL('new_table')); @@ -125,7 +126,7 @@ public function testCreateTable() 'nullable' => false, 'default' => 'CURRENT_TIMESTAMP', 'disabled' => false, - 'onCreate' => NULL, + 'onCreate' => null, ], 'integer' => [ @@ -135,9 +136,9 @@ public function testCreateTable() 'unsigned' => false, 'nullable' => false, 'identity' => true, - 'default' => NULL, + 'default' => null, 'disabled' => false, - 'onCreate' => NULL, + 'onCreate' => null, ], 'decimal' => [ @@ -147,9 +148,9 @@ public function testCreateTable() 'precision' => '25', 'unsigned' => false, 'nullable' => false, - 'default' => NULL, + 'default' => null, 'disabled' => false, - 'onCreate' => NULL, + 'onCreate' => null, ], ], $tables['First_Module']['new_table']['columns'] @@ -201,7 +202,7 @@ public function testCreateTable() ); } - public function testDropTable() + public function testDropTable() : void { $this->model->setModuleName('Old_Module'); $this->model->createTable($this->getCreateTableDDL('old_table')); @@ -210,7 +211,7 @@ public function testDropTable() self::assertTrue($this->model->getTables()['New_Module']['old_table']['disabled']); } - public function testDropTableInSameModule() + public function testDropTableInSameModule() : void { $this->model->setModuleName('Old_Module'); $this->model->createTable($this->getCreateTableDDL('old_table')); diff --git a/app/code/Magento/Framework/Test/Unit/Setup/SchemaPersistorTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php similarity index 94% rename from app/code/Magento/Framework/Test/Unit/Setup/SchemaPersistorTest.php rename to lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php index 56f04f4c7ba77..cc88af15a262b 100644 --- a/app/code/Magento/Framework/Test/Unit/Setup/SchemaPersistorTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/SchemaPersistorTest.php @@ -3,8 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace Magento\Framework\Test\Unit\Setup; +namespace Magento\Framework\Setup\Test\Unit; use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Setup\SchemaListener; @@ -14,7 +15,7 @@ /** * Unit test for schema persistor. * - * @package Magento\Framework\Test\Unit\Setup + * @package Magento\Framework\Setup\Test\Unit */ class SchemaPersistorTest extends \PHPUnit\Framework\TestCase { @@ -38,7 +39,7 @@ class SchemaPersistorTest extends \PHPUnit\Framework\TestCase */ private $xmlPersistor; - protected function setUp() + protected function setUp() : void { $this->componentRegistrarMock = $this->getMockBuilder(ComponentRegistrar::class) ->disableOriginalConstructor() @@ -60,7 +61,7 @@ protected function setUp() * @param array $tables * @param string $expectedXML */ - public function testPersist(array $tables, $expectedXML) + public function testPersist(array $tables, $expectedXML) : void { $moduleName = 'First_Module'; /** @var SchemaListener|\PHPUnit_Framework_MockObject_MockObject $schemaListenerMock */ @@ -88,7 +89,7 @@ public function testPersist(array $tables, $expectedXML) * * @return array */ - public function schemaListenerTablesDataProvider() + public function schemaListenerTablesDataProvider() : array { return [ [ @@ -143,6 +144,7 @@ public function schemaListenerTablesDataProvider() ] ] ], + // @codingStandardsIgnoreStart 'XMLResult' => '<?xml version="1.0"?> <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> @@ -161,6 +163,7 @@ public function schemaListenerTablesDataProvider() </index> </table> </schema>' + // @codingStandardsIgnoreEnd ] ]; } diff --git a/lib/internal/Magento/Framework/Test/Unit/TranslateTest.php b/lib/internal/Magento/Framework/Test/Unit/TranslateTest.php deleted file mode 100644 index cbf3f89717bc6..0000000000000 --- a/lib/internal/Magento/Framework/Test/Unit/TranslateTest.php +++ /dev/null @@ -1,356 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Framework\Test\Unit; - -use Magento\Framework\Serialize\SerializerInterface; -use Magento\Framework\Translate; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class TranslateTest extends \PHPUnit\Framework\TestCase -{ - /** @var Translate */ - protected $translate; - - /** @var \Magento\Framework\View\DesignInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $viewDesign; - - /** @var \Magento\Framework\Cache\FrontendInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $cache; - - /** @var \Magento\Framework\View\FileSystem|\PHPUnit_Framework_MockObject_MockObject */ - protected $viewFileSystem; - - /** @var \Magento\Framework\Module\ModuleList|\PHPUnit_Framework_MockObject_MockObject */ - protected $moduleList; - - /** @var \Magento\Framework\Module\Dir\Reader|\PHPUnit_Framework_MockObject_MockObject */ - protected $modulesReader; - - /** @var \Magento\Framework\App\ScopeResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $scopeResolver; - - /** @var \Magento\Framework\Translate\ResourceInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $resource; - - /** @var \Magento\Framework\Locale\ResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $locale; - - /** @var \Magento\Framework\App\State|\PHPUnit_Framework_MockObject_MockObject */ - protected $appState; - - /** @var \Magento\Framework\Filesystem|\PHPUnit_Framework_MockObject_MockObject */ - protected $filesystem; - - /** @var \Magento\Framework\App\RequestInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $request; - - /** @var \Magento\Framework\File\Csv|\PHPUnit_Framework_MockObject_MockObject */ - protected $csvParser; - - /** @var \Magento\Framework\App\Language\Dictionary|\PHPUnit_Framework_MockObject_MockObject */ - protected $packDictionary; - - /** @var \Magento\Framework\Filesystem\Directory\ReadInterface|\PHPUnit_Framework_MockObject_MockObject */ - protected $directory; - - protected function setUp() - { - $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); - $this->viewDesign = $this->createMock(\Magento\Framework\View\DesignInterface::class); - $this->cache = $this->createMock(\Magento\Framework\Cache\FrontendInterface::class); - $this->viewFileSystem = $this->createMock(\Magento\Framework\View\FileSystem::class); - $this->moduleList = $this->createMock(\Magento\Framework\Module\ModuleList::class); - $this->modulesReader = $this->createMock(\Magento\Framework\Module\Dir\Reader::class); - $this->scopeResolver = $this->createMock(\Magento\Framework\App\ScopeResolverInterface::class); - $this->resource = $this->createMock(\Magento\Framework\Translate\ResourceInterface::class); - $this->locale = $this->createMock(\Magento\Framework\Locale\ResolverInterface::class); - $this->appState = $this->createMock(\Magento\Framework\App\State::class); - $this->request = $this->getMockForAbstractClass( - \Magento\Framework\App\RequestInterface::class, - [], - '', - false, - false, - true, - ['getParam', 'getControllerModule'] - ); - $this->csvParser = $this->createMock(\Magento\Framework\File\Csv::class); - $this->packDictionary = $this->createMock(\Magento\Framework\App\Language\Dictionary::class); - $this->directory = $this->createMock(\Magento\Framework\Filesystem\Directory\ReadInterface::class); - $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); - $filesystem->expects($this->once())->method('getDirectoryRead')->will($this->returnValue($this->directory)); - - $this->translate = new Translate( - $this->viewDesign, - $this->cache, - $this->viewFileSystem, - $this->moduleList, - $this->modulesReader, - $this->scopeResolver, - $this->resource, - $this->locale, - $this->appState, - $filesystem, - $this->request, - $this->csvParser, - $this->packDictionary - ); - - $serializerMock = $this->createMock(SerializerInterface::class); - $serializerMock->method('serialize') - ->willReturnCallback(function ($data) { - return json_encode($data); - }); - $serializerMock->method('unserialize') - ->willReturnCallback(function ($string) { - return json_decode($string, true); - }); - $objectManager->setBackwardCompatibleProperty( - $this->translate, - 'serializer', - $serializerMock - ); - } - - /** - * @param string $area - * @param bool $forceReload - * @param array $cachedData - * @return void - * @dataProvider dataProviderLoadDataCachedTranslation - */ - public function testLoadDataCachedTranslation($area, $forceReload, array $cachedData) - { - $this->expectsSetConfig('Magento/luma'); - - $this->cache->expects($this->once()) - ->method('load') - ->willReturn(json_encode($cachedData)); - - $this->appState->expects($this->exactly($area ? 0 : 1)) - ->method('getAreaCode') - ->willReturn('frontend'); - - $this->translate->loadData($area, $forceReload); - $this->assertEquals($cachedData, $this->translate->getData()); - } - - /** - * @return array - */ - public function dataProviderLoadDataCachedTranslation() - { - $cachedData = ['cached 1' => 'translated 1', 'cached 2' => 'translated 2']; - return [ - ['adminhtml', false, $cachedData], - ['frontend', false, $cachedData], - [null, false, $cachedData], - ]; - } - - /** - * @param string $area - * @param bool $forceReload - * @return void - * @dataProvider dataProviderForTestLoadData - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function testLoadData($area, $forceReload) - { - $this->expectsSetConfig('Magento/luma'); - - $this->appState->expects($this->exactly($area ? 0 : 1)) - ->method('getAreaCode') - ->willReturn('frontend'); - - $this->cache->expects($this->exactly($forceReload ? 0 : 1)) - ->method('load') - ->willReturn(false); - - $this->directory->expects($this->any())->method('isExist')->willReturn(true); - - // _loadModuleTranslation() - $modules = ['some_module', 'other_module', 'another_module', 'current_module']; - $this->request->expects($this->any()) - ->method('getControllerModule') - ->willReturn('current_module'); - $this->moduleList->expects($this->once())->method('getNames')->willReturn($modules); - $moduleData = [ - 'module original' => 'module translated', - 'module theme' => 'module-theme original translated', - 'module pack' => 'module-pack original translated', - 'module db' => 'module-db original translated', - ]; - $this->modulesReader->expects($this->any())->method('getModuleDir')->willReturn('/app/module'); - $themeData = [ - 'theme original' => 'theme translated', - 'module theme' => 'theme translated overwrite', - 'module pack' => 'theme-pack translated overwrite', - 'module db' => 'theme-db translated overwrite', - ]; - $this->csvParser->expects($this->any()) - ->method('getDataPairs') - ->willReturnMap( - [ - ['/app/module/en_US.csv', 0, 1, $moduleData], - ['/app/module/en_GB.csv', 0, 1, $moduleData], - ['/theme.csv', 0, 1, $themeData], - ] - ); - - // _loadPackTranslation - $packData = [ - 'pack original' => 'pack translated', - 'module pack' => 'pack translated overwrite', - 'module db' => 'pack-db translated overwrite', - ]; - $this->packDictionary->expects($this->once())->method('getDictionary')->willReturn($packData); - - // _loadThemeTranslation() - $this->viewFileSystem->expects($this->any()) - ->method('getLocaleFileName') - ->will($this->returnValue('/theme.csv')); - - // _loadDbTranslation() - $dbData = [ - 'db original' => 'db translated', - 'module db' => 'db translated overwrite', - ]; - $this->resource->expects($this->any())->method('getTranslationArray')->willReturn($dbData); - - $this->cache->expects($this->exactly($forceReload ? 0 : 1))->method('save'); - - $this->translate->loadData($area, $forceReload); - - $expected = [ - 'module original' => 'module translated', - 'module theme' => 'theme translated overwrite', - 'module pack' => 'theme-pack translated overwrite', - 'module db' => 'db translated overwrite', - 'theme original' => 'theme translated', - 'pack original' => 'pack translated', - 'db original' => 'db translated', - ]; - $this->assertEquals($expected, $this->translate->getData()); - } - - /** - * @return array - */ - public function dataProviderForTestLoadData() - { - return [ - ['adminhtml', true], - ['adminhtml', false], - ['frontend', true], - ['frontend', false], - [null, true], - [null, false], - ]; - } - - /** - * @param $data - * @param $result - * @return void - * @dataProvider dataProviderForTestGetData - */ - public function testGetData($data, $result) - { - $this->cache->expects($this->once()) - ->method('load') - ->will($this->returnValue(json_encode($data))); - $this->expectsSetConfig('themeId'); - $this->translate->loadData('frontend'); - $this->assertEquals($result, $this->translate->getData()); - } - - /** - * @return array - */ - public function dataProviderForTestGetData() - { - $data = ['original 1' => 'translated 1', 'original 2' => 'translated 2']; - return [ - [$data, $data], - [null, []], - ]; - } - - public function testGetLocale() - { - $this->locale->expects($this->once())->method('getLocale')->will($this->returnValue('en_US')); - $this->assertEquals('en_US', $this->translate->getLocale()); - - $this->locale->expects($this->never())->method('getLocale'); - $this->assertEquals('en_US', $this->translate->getLocale()); - - $this->locale->expects($this->never())->method('getLocale'); - $this->translate->setLocale('en_GB'); - $this->assertEquals('en_GB', $this->translate->getLocale()); - } - - public function testSetLocale() - { - $this->translate->setLocale('en_GB'); - $this->locale->expects($this->never())->method('getLocale'); - $this->assertEquals('en_GB', $this->translate->getLocale()); - } - - public function testGetTheme() - { - $this->request->expects($this->at(0))->method('getParam')->with('theme')->will($this->returnValue('')); - - $requestTheme = ['theme_title' => 'Theme Title']; - $this->request->expects($this->at(1))->method('getParam')->with('theme') - ->will($this->returnValue($requestTheme)); - - $this->assertEquals('theme', $this->translate->getTheme()); - $this->assertEquals('themeTheme Title', $this->translate->getTheme()); - } - - public function testLoadDataNoTheme() - { - $forceReload = true; - $this->expectsSetConfig(null, null); - $this->moduleList->expects($this->once())->method('getNames')->will($this->returnValue([])); - $this->appState->expects($this->once())->method('getAreaCode')->will($this->returnValue('frontend')); - $this->packDictionary->expects($this->once())->method('getDictionary')->will($this->returnValue([])); - $this->resource->expects($this->any())->method('getTranslationArray')->will($this->returnValue([])); - $this->assertEquals($this->translate, $this->translate->loadData(null, $forceReload)); - } - - /** - * Declare calls expectation for setConfig() method - */ - protected function expectsSetConfig($themeId, $localeCode = 'en_US') - { - $this->locale->expects($this->any())->method('getLocale')->will($this->returnValue($localeCode)); - $scope = new \Magento\Framework\DataObject(['code' => 'frontendCode', 'id' => 1]); - $scopeAdmin = new \Magento\Framework\DataObject(['code' => 'adminCode', 'id' => 0]); - $this->scopeResolver->expects($this->any()) - ->method('getScope') - ->will( - $this->returnValueMap( - [ - [null, $scope], - ['admin', $scopeAdmin], - ] - ) - ); - $designTheme = $this->getMockBuilder(\Magento\Theme\Model\Theme::class) - ->disableOriginalConstructor() - ->getMock(); - - $designTheme->expects($this->once()) - ->method('getThemePath') - ->willReturn($themeId); - - $this->viewDesign->expects($this->any())->method('getDesignTheme')->will($this->returnValue($designTheme)); - } -} diff --git a/lib/internal/Magento/Framework/Translate.php b/lib/internal/Magento/Framework/Translate.php index 2f80ab2befbf8..ffa8e25031064 100644 --- a/lib/internal/Magento/Framework/Translate.php +++ b/lib/internal/Magento/Framework/Translate.php @@ -7,6 +7,9 @@ namespace Magento\Framework; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Filesystem\DriverInterface; /** * Translate library @@ -120,6 +123,11 @@ class Translate implements \Magento\Framework\TranslateInterface */ private $serializer; + /** + * @var DriverInterface + */ + private $fileDriver; + /** * @param \Magento\Framework\View\DesignInterface $viewDesign * @param \Magento\Framework\Cache\FrontendInterface $cache @@ -134,6 +142,7 @@ class Translate implements \Magento\Framework\TranslateInterface * @param \Magento\Framework\App\RequestInterface $request * @param \Magento\Framework\File\Csv $csvParser * @param \Magento\Framework\App\Language\Dictionary $packDictionary + * @param DriverInterface|null $fileDriver * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -150,7 +159,8 @@ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Framework\App\RequestInterface $request, \Magento\Framework\File\Csv $csvParser, - \Magento\Framework\App\Language\Dictionary $packDictionary + \Magento\Framework\App\Language\Dictionary $packDictionary, + DriverInterface $fileDriver = null ) { $this->_viewDesign = $viewDesign; $this->_cache = $cache; @@ -165,6 +175,8 @@ public function __construct( $this->directory = $filesystem->getDirectoryRead(DirectoryList::ROOT); $this->_csvParser = $csvParser; $this->packDictionary = $packDictionary; + $this->fileDriver = $fileDriver + ?? ObjectManager::getInstance()->get(File::class); $this->_config = [ self::CONFIG_AREA_KEY => null, @@ -400,7 +412,7 @@ protected function _getThemeTranslationFile($locale) protected function _getFileData($file) { $data = []; - if ($this->directory->isExist($this->directory->getRelativePath($file))) { + if ($this->fileDriver->isExists($file)) { $this->_csvParser->setDelimiter(','); $data = $this->_csvParser->getDataPairs($file); } diff --git a/lib/internal/Magento/Framework/View/Asset/Merged.php b/lib/internal/Magento/Framework/View/Asset/Merged.php index 5b206b235eb11..302eb1226b8ef 100644 --- a/lib/internal/Magento/Framework/View/Asset/Merged.php +++ b/lib/internal/Magento/Framework/View/Asset/Merged.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\View\Asset; +use Magento\Framework\App\ObjectManager; + /** * \Iterator that aggregates one or more assets and provides a single public file with equivalent behavior */ @@ -40,27 +42,39 @@ class Merged implements \Iterator */ protected $contentType; + /** + * @var \Magento\Framework\App\View\Deployment\Version\StorageInterface + */ + private $versionStorage; + /** * @var bool */ protected $isInitialized = false; /** + * Merged constructor. + * * @param \Psr\Log\LoggerInterface $logger * @param MergeStrategyInterface $mergeStrategy * @param \Magento\Framework\View\Asset\Repository $assetRepo * @param MergeableInterface[] $assets + * @param \Magento\Framework\App\View\Deployment\Version\StorageInterface $versionStorage * @throws \InvalidArgumentException */ public function __construct( \Psr\Log\LoggerInterface $logger, MergeStrategyInterface $mergeStrategy, \Magento\Framework\View\Asset\Repository $assetRepo, - array $assets + array $assets, + \Magento\Framework\App\View\Deployment\Version\StorageInterface $versionStorage = null ) { $this->logger = $logger; $this->mergeStrategy = $mergeStrategy; $this->assetRepo = $assetRepo; + $this->versionStorage = $versionStorage ?: ObjectManager::getInstance()->get( + \Magento\Framework\App\View\Deployment\Version\StorageInterface::class + ); if (!$assets) { throw new \InvalidArgumentException('At least one asset has to be passed for merging.'); @@ -116,6 +130,12 @@ private function createMergedAsset(array $assets) $paths[] = $asset->getPath(); } $paths = array_unique($paths); + + $version = $this->versionStorage->load(); + if ($version) { + $paths[] = $version; + } + $filePath = md5(implode('|', $paths)) . '.' . $this->contentType; return $this->assetRepo->createArbitrary($filePath, self::getRelativeDir()); } diff --git a/lib/internal/Magento/Framework/View/Element/UiComponent/Config/FileCollector/AggregatedFileCollector.php b/lib/internal/Magento/Framework/View/Element/UiComponent/Config/FileCollector/AggregatedFileCollector.php index 17a320164dad3..9ba7833a355d7 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponent/Config/FileCollector/AggregatedFileCollector.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponent/Config/FileCollector/AggregatedFileCollector.php @@ -75,7 +75,7 @@ public function collectFiles($searchPattern = null) } $files = $this->collectorAggregated->getFiles($this->design->getDesignTheme(), $searchPattern); foreach ($files as $file) { - $fullFileName = $file->getFileName(); + $fullFileName = $file->getFilename(); $fileDir = dirname($fullFileName); $fileName = basename($fullFileName); $dirRead = $this->readFactory->create($fileDir); diff --git a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php index fe5c94ed21b30..a0cdbfb7d8fe7 100644 --- a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php +++ b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php @@ -443,6 +443,9 @@ public function load($handles = []) if ($result) { $this->addUpdate($result); $this->pageLayout = $this->_loadCache($cacheIdPageLayout); + foreach ($this->getHandles() as $handle) { + $this->allHandles[$handle] = $this->handleProcessed; + } return $this; } @@ -672,7 +675,7 @@ public function getFileLayoutUpdatesXml() $result = $this->_loadXmlString($result); } else { $result = $this->_loadFileLayoutUpdatesXml(); - $this->_saveCache($result->asXml(), $cacheId); + $this->_saveCache($result->asXML(), $cacheId); } $this->layoutUpdatesCache = $result; return $result; diff --git a/lib/internal/Magento/Framework/View/Page/Config/Reader/Head.php b/lib/internal/Magento/Framework/View/Page/Config/Reader/Head.php index 8328f03fd6db5..71a2951b40420 100644 --- a/lib/internal/Magento/Framework/View/Page/Config/Reader/Head.php +++ b/lib/internal/Magento/Framework/View/Page/Config/Reader/Head.php @@ -138,6 +138,6 @@ private function setMetadata($pageConfigStructure, $node) $metadataName = $node->getAttribute('name'); } - $pageConfigStructure->setMetaData($metadataName, $node->getAttribute('content')); + $pageConfigStructure->setMetadata($metadataName, $node->getAttribute('content')); } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergedTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergedTest.php index 164a2ca4d4d1b..52b45a510e722 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergedTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/MergedTest.php @@ -12,6 +12,7 @@ use Magento\Framework\View\Asset\Repository as AssetRepository; use Magento\Framework\View\Asset\MergeableInterface; use Magento\Framework\View\Asset\MergeStrategyInterface; +use Magento\Framework\App\View\Deployment\Version\StorageInterface; /** * Class MergedTest @@ -43,6 +44,11 @@ class MergedTest extends \PHPUnit\Framework\TestCase */ private $assetRepo; + /** + * @var StorageInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $versionStorage; + protected function setUp() { $this->assetJsOne = $this->getMockForAbstractClass(MergeableInterface::class); @@ -66,6 +72,7 @@ protected function setUp() $this->assetRepo = $this->getMockBuilder(AssetRepository::class) ->disableOriginalConstructor() ->getMock(); + $this->versionStorage = $this->createMock(StorageInterface::class); } /** @@ -74,7 +81,13 @@ protected function setUp() */ public function testConstructorNothingToMerge() { - new \Magento\Framework\View\Asset\Merged($this->logger, $this->mergeStrategy, $this->assetRepo, []); + new \Magento\Framework\View\Asset\Merged( + $this->logger, + $this->mergeStrategy, + $this->assetRepo, + [], + $this->versionStorage + ); } /** @@ -90,6 +103,7 @@ public function testConstructorRequireMergeInterface() 'mergeStrategy' => $this->mergeStrategy, 'assetRepo' => $this->assetRepo, 'assets' => [$this->assetJsOne, $assetUrl], + 'versionStorage' => $this->versionStorage, ]); } @@ -109,6 +123,7 @@ public function testConstructorIncompatibleContentTypes() 'mergeStrategy' => $this->mergeStrategy, 'assetRepo' => $this->assetRepo, 'assets' => [$this->assetJsOne, $assetCss], + 'versionStorage' => $this->versionStorage, ]); } @@ -124,6 +139,7 @@ public function testIteratorInterfaceMerge() 'mergeStrategy' => $this->mergeStrategy, 'assetRepo' => $this->assetRepo, 'assets' => $assets, + 'versionStorage' => $this->versionStorage, ]); $mergedAsset = $this->createMock(\Magento\Framework\View\Asset\File::class); @@ -158,6 +174,7 @@ public function testIteratorInterfaceMergeFailure() 'mergeStrategy' => $this->mergeStrategy, 'assetRepo' => $this->assetRepo, 'assets' => [$this->assetJsOne, $this->assetJsTwo, $assetBroken], + 'versionStorage' => $this->versionStorage, ]); $this->logger->expects($this->once())->method('critical')->with($this->identicalTo($mergeError)); diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php index 4515a29c65e4f..909748722a081 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php @@ -57,7 +57,7 @@ public function testIsCurrentIfIsset() /** @var \Magento\Framework\View\Element\Html\Link\Current $link */ $link = $this->_objectManager->getObject(\Magento\Framework\View\Element\Html\Link\Current::class); $link->setCurrent(true); - $this->assertTrue($link->IsCurrent()); + $this->assertTrue($link->isCurrent()); } public function testIsCurrent() diff --git a/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php b/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php index c0300164e26fe..f8a8939bbfe36 100755 --- a/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php @@ -273,7 +273,7 @@ public function testGenerateXml() ->with($this->equalTo([])) ->will($this->returnSelf()); $this->assertSame($this->model, $this->model->generateXml()); - $this->assertSame('<some_update>123</some_update>', $this->model->getNode('some_update')->asXml()); + $this->assertSame('<some_update>123</some_update>', $this->model->getNode('some_update')->asXML()); } public function testGetChildBlock() diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index 598d4546006bd..c360d57be107f 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -22,13 +22,13 @@ "ext-xsl": "*", "ext-bcmath": "*", "lib-libxml": "*", - "colinmollenhour/php-redis-session-abstract": "~1.3.8", + "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.6", "magento/zendframework1": "~1.14.0", "monolog/monolog": "^1.17", "oyejorge/less.php": "~1.7.0", - "symfony/console": "~4.0.0", - "symfony/process": "~4.0.0", + "symfony/console": "~4.1.0", + "symfony/process": "~4.1.0", "tedivm/jshrink": "~1.3.0", "zendframework/zend-code": "~3.3.0", "zendframework/zend-crypt": "^2.6.0", diff --git a/lib/web/css/source/lib/_forms.less b/lib/web/css/source/lib/_forms.less index 800054e58c3dd..b1c7a49da4a7a 100644 --- a/lib/web/css/source/lib/_forms.less +++ b/lib/web/css/source/lib/_forms.less @@ -465,11 +465,9 @@ .lib-css(margin, @_margin); .lib-css(padding, @_padding); letter-spacing: -.31em; - //word-spacing: -.43em; > * { letter-spacing: normal; - //word-spacing: normal; } > .legend { diff --git a/lib/web/css/source/lib/_rating.less b/lib/web/css/source/lib/_rating.less index e585e4489d65e..535fa44616039 100644 --- a/lib/web/css/source/lib/_rating.less +++ b/lib/web/css/source/lib/_rating.less @@ -38,7 +38,7 @@ input[type="radio"] { .lib-visually-hidden(); - &:focus, + &:hover, &:checked { + label { &:before { diff --git a/lib/web/css/source/lib/_tables.less b/lib/web/css/source/lib/_tables.less index 9c37e17b4fe82..43b63152946f8 100644 --- a/lib/web/css/source/lib/_tables.less +++ b/lib/web/css/source/lib/_tables.less @@ -530,7 +530,7 @@ display: block; .lib-css(padding, @_table-responsive-cell-padding); - &:before { + &[data-th]:before { .lib-css(padding-right, @table-cell__padding-horizontal); content: attr(data-th)': '; display: inline-block; diff --git a/lib/web/css/source/lib/_utilities.less b/lib/web/css/source/lib/_utilities.less index 08a82494b3e5c..222eb4741e85e 100644 --- a/lib/web/css/source/lib/_utilities.less +++ b/lib/web/css/source/lib/_utilities.less @@ -260,9 +260,8 @@ @_line-height: normal ) { .lib-font-size(@_font-size); - font-size: @_font-size; + .lib-line-height(@_line-height); letter-spacing: normal; - line-height: @_line-height; } // diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 6258b3c627370..d08819ebe94aa 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -990,6 +990,12 @@ }, $.mage.__('Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.') //eslint-disable-line max-len ], + 'validate-not-number-first': [ + function (value) { + return $.mage.isEmptyNoTrim(value) || /^[^0-9-\.].*$/.test(value.trim()); + }, + $.mage.__('First character must be letter.') + ], 'validate-date': [ function (value, params, additionalParams) { var test = moment(value, additionalParams.dateFormat); diff --git a/setup/src/Magento/Setup/Model/PhpReadinessCheck.php b/setup/src/Magento/Setup/Model/PhpReadinessCheck.php index c2bdafa8df82e..2c4967c4c2ffd 100644 --- a/setup/src/Magento/Setup/Model/PhpReadinessCheck.php +++ b/setup/src/Magento/Setup/Model/PhpReadinessCheck.php @@ -286,7 +286,7 @@ private function checkPopulateRawPostSetting() $data = []; $error = false; - $iniSetting = intVal(ini_get('always_populate_raw_post_data')); + $iniSetting = intval(ini_get('always_populate_raw_post_data')); $checkVersionConstraint = $this->versionParser->parseConstraints('~5.6.0'); $normalizedPhpVersion = $this->getNormalizedCurrentPhpVersion(PHP_VERSION); @@ -302,7 +302,7 @@ private function checkPopulateRawPostSetting() Please open your php.ini file and set always_populate_raw_post_data to -1. If you need more help please call your hosting provider.', PHP_VERSION, - intVal(ini_get('always_populate_raw_post_data')) + intval(ini_get('always_populate_raw_post_data')) ); $data['always_populate_raw_post_data'] = [ diff --git a/setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php b/setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php index 7759eb51a52ad..d78e259ec06e0 100644 --- a/setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php +++ b/setup/src/Magento/Setup/Module/Di/Code/Reader/ClassesScanner.php @@ -117,7 +117,7 @@ private function extract(\RecursiveIteratorIterator $recursiveIterator) /** * @param array $classNames * @param string $fileItemPath - * @return bool Whether the clas is included or not + * @return bool Whether the class is included or not */ private function includeClasses(array $classNames, $fileItemPath) { diff --git a/setup/src/Magento/Setup/Module/Di/Code/Scanner/ConfigurationScanner.php b/setup/src/Magento/Setup/Module/Di/Code/Scanner/ConfigurationScanner.php index fac5b1b4aec98..747865f7cfef9 100644 --- a/setup/src/Magento/Setup/Module/Di/Code/Scanner/ConfigurationScanner.php +++ b/setup/src/Magento/Setup/Module/Di/Code/Scanner/ConfigurationScanner.php @@ -28,7 +28,7 @@ public function __construct( * * @param string $fileName * - * @return array array of paths to the configuration files + * @return array of paths to the configuration files */ public function scan($fileName) {