diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml index 4f0e9bb000a27..2ac03bc3da675 100644 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml @@ -31,5 +31,46 @@ + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?("[\w_]+":\s+"[^"]*?"\s+)};#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"user":\s+"[\w\d]{64}"#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"secure_base_url":\s+"http(s)?\\\\u003A\\\\u002F\\\\u002F.+?\\\\u002F"#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"version":\s+"[^\s]+"#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"product_edition":\s+"(Community|Enterprise|B2B)"#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"mode":\s+"default|developer|production"#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"store_name_default":\s+".*?"#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"admin_user_created":\s+".+?"#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"admin_user_logdate":\s+".+?"#s + $pageSource + + + #var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?"admin_user_role_name":\s+".+?"#s + $pageSource + diff --git a/app/code/Magento/AdminAnalytics/ViewModel/Metadata.php b/app/code/Magento/AdminAnalytics/ViewModel/Metadata.php index 9b1accbe0c823..15d4afef086cd 100644 --- a/app/code/Magento/AdminAnalytics/ViewModel/Metadata.php +++ b/app/code/Magento/AdminAnalytics/ViewModel/Metadata.php @@ -3,12 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\AdminAnalytics\ViewModel; +use Magento\Config\Model\Config\Backend\Admin\Custom; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ProductMetadataInterface; use Magento\Backend\Model\Auth\Session; use Magento\Framework\App\State; use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Store\Model\Information; /** * Gets user version and mode @@ -30,19 +36,27 @@ class Metadata implements ArgumentInterface */ private $productMetadata; + /** + * @var ScopeConfigInterface + */ + private $config; + /** * @param ProductMetadataInterface $productMetadata * @param Session $authSession * @param State $appState + * @param ScopeConfigInterface $config */ public function __construct( ProductMetadataInterface $productMetadata, Session $authSession, - State $appState + State $appState, + ScopeConfigInterface $config ) { $this->productMetadata = $productMetadata; $this->authSession = $authSession; $this->appState = $appState; + $this->config = $config; } /** @@ -55,6 +69,16 @@ public function getMagentoVersion() :string return $this->productMetadata->getVersion(); } + /** + * Get product edition + * + * @return string + */ + public function getProductEdition(): string + { + return $this->productMetadata->getEdition(); + } + /** * Get current user id (hash generated from email) * @@ -62,8 +86,9 @@ public function getMagentoVersion() :string */ public function getCurrentUser() :string { - return hash('sha512', 'ADMIN_USER' . $this->authSession->getUser()->getEmail()); + return hash('sha256', 'ADMIN_USER' . $this->authSession->getUser()->getEmail()); } + /** * Get Magento mode that the user is using * @@ -73,4 +98,62 @@ public function getMode() :string { return $this->appState->getMode(); } + + /** + * Get created date for current user + * + * @return string + */ + public function getCurrentUserCreatedDate(): string + { + return $this->authSession->getUser()->getCreated(); + } + + /** + * Get log date for current user + * + * @return string|null + */ + public function getCurrentUserLogDate(): ?string + { + return $this->authSession->getUser()->getLogdate(); + } + + /** + * Get secure base URL + * + * @param string $scope + * @param string|null $scopeCode + * @return string|null + */ + public function getSecureBaseUrlForScope( + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + ?string $scopeCode = null + ): ?string { + return $this->config->getValue(Custom::XML_PATH_SECURE_BASE_URL, $scope, $scopeCode); + } + + /** + * Get store name + * + * @param string $scope + * @param string|null $scopeCode + * @return string|null + */ + public function getStoreNameForScope( + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + ?string $scopeCode = null + ): ?string { + return $this->config->getValue(Information::XML_PATH_STORE_INFO_NAME, $scope, $scopeCode); + } + + /** + * Get current user role name + * + * @return string + */ + public function getCurrentUserRoleName(): string + { + return $this->authSession->getUser()->getRole()->getRoleName(); + } } diff --git a/app/code/Magento/AdminAnalytics/composer.json b/app/code/Magento/AdminAnalytics/composer.json index cf60b1d88ae55..f97d33c26f35f 100644 --- a/app/code/Magento/AdminAnalytics/composer.json +++ b/app/code/Magento/AdminAnalytics/composer.json @@ -9,6 +9,7 @@ "magento/framework": "*", "magento/module-backend": "*", "magento/module-config": "*", + "magento/module-store": "*", "magento/module-ui": "*", "magento/module-release-notification": "*" }, diff --git a/app/code/Magento/AdminAnalytics/etc/adminhtml/csp_whitelist.xml b/app/code/Magento/AdminAnalytics/etc/adminhtml/csp_whitelist.xml new file mode 100644 index 0000000000000..adc65acd3b040 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/etc/adminhtml/csp_whitelist.xml @@ -0,0 +1,39 @@ + + + + + + + *.aptrinsic.com + + + + + *.aptrinsic.com + fonts.googleapis.com + + + + + *.aptrinsic.com + storage.googleapis.com + + + + + *.aptrinsic.com + + + + + fonts.gstatic.com + + + + diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/layout/default.xml b/app/code/Magento/AdminAnalytics/view/adminhtml/layout/default.xml index 7e379a17c78d7..8fcda98e604bc 100644 --- a/app/code/Magento/AdminAnalytics/view/adminhtml/layout/default.xml +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/layout/default.xml @@ -10,7 +10,7 @@ - //assets.adobedtm.com/launch-EN30eb7ffa064444f1b8b0368ef38fd3a9.min.js + //assets.adobedtm.com/a7d65461e54e/37baabec1b6e/launch-177bc126c8e6.min.js Magento\AdminAnalytics\ViewModel\Metadata diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml index bfe58de1eac5f..4b155e1a5ae5b 100644 --- a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml @@ -19,11 +19,20 @@ false ) ?> -getMetadata(); +$scriptString = ' var adminAnalyticsMetadata = { - "version": "' . $block->escapeJs($block->getMetadata()->getMagentoVersion()) . '", - "user": "' . $block->escapeJs($block->getMetadata()->getCurrentUser()) . '", - "mode": "' . $block->escapeJs($block->getMetadata()->getMode()) . '" + "secure_base_url": "' . $block->escapeJs($metadata->getSecureBaseUrlForScope()) . '", + "version": "' . $block->escapeJs($metadata->getMagentoVersion()) . '", + "product_edition": "' . $block->escapeJs($metadata->getProductEdition()) . '", + "user": "' . $block->escapeJs($metadata->getCurrentUser()) . '", + "mode": "' . $block->escapeJs($metadata->getMode()) . '", + "store_name_default": "' . $block->escapeJs($metadata->getStoreNameForScope()) . '", + "admin_user_created": "' . $block->escapeJs($metadata->getCurrentUserCreatedDate()) . '", + "admin_user_logdate": "' . $block->escapeJs($metadata->getCurrentUserLogDate()) . '", + "admin_user_role_name": "' . $block->escapeJs($metadata->getCurrentUserRoleName()) . '" }; '; ?> diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index af43562984134..b63141b510b85 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -486,7 +486,8 @@ private function fetchTierPrices(array $productIds): array ) ->where( 'ap.' . $productEntityLinkField . ' IN (?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); if ($priceFromFilter !== null) { diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontCaptchaOnCustomerLoginTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontCaptchaOnCustomerLoginTest.xml new file mode 100644 index 0000000000000..52ab08da88bde --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontCaptchaOnCustomerLoginTest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + <description value="Check CAPTCHA on Storefront Login Page."/> + <severity value="MAJOR"/> + <testCaseId value="MC-39491" /> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/composer.json b/app/code/Magento/AwsS3/composer.json index 6e72ac37f8ba6..77ed75862d192 100644 --- a/app/code/Magento/AwsS3/composer.json +++ b/app/code/Magento/AwsS3/composer.json @@ -1,5 +1,5 @@ { - "name": "magento/module-aws-s-3", + "name": "magento/module-aws-s3", "description": "N/A", "config": { "sort-packages": true diff --git a/app/code/Magento/Backend/App/Area/FrontNameResolver.php b/app/code/Magento/Backend/App/Area/FrontNameResolver.php index 6c586781f2d81..a927f52b59d95 100644 --- a/app/code/Magento/Backend/App/Area/FrontNameResolver.php +++ b/app/code/Magento/Backend/App/Area/FrontNameResolver.php @@ -123,7 +123,13 @@ public function isHostBackend() if ($this->scopeConfig->getValue(self::XML_PATH_USE_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE)) { $backendUrl = $this->scopeConfig->getValue(self::XML_PATH_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE); } else { - $backendUrl = $this->scopeConfig->getValue(Store::XML_PATH_UNSECURE_BASE_URL, ScopeInterface::SCOPE_STORE); + $backendUrl = $this->config->getValue(Store::XML_PATH_UNSECURE_BASE_URL); + if ($backendUrl === null) { + $backendUrl = $this->scopeConfig->getValue( + Store::XML_PATH_UNSECURE_BASE_URL, + ScopeInterface::SCOPE_STORE + ); + } } $host = $this->request->getServer('HTTP_HOST', ''); return stripos($this->getHostWithPort($backendUrl), (string) $host) !== false; diff --git a/app/code/Magento/Backend/Block/Widget/Button.php b/app/code/Magento/Backend/Block/Widget/Button.php index 3b5eca6a61779..cb8269f692f02 100644 --- a/app/code/Magento/Backend/Block/Widget/Button.php +++ b/app/code/Magento/Backend/Block/Widget/Button.php @@ -5,9 +5,9 @@ */ namespace Magento\Backend\Block\Widget; +use Magento\Backend\Block\Template\Context; use Magento\Framework\App\ObjectManager; use Magento\Framework\Math\Random; -use Magento\Backend\Block\Template\Context; use Magento\Framework\View\Helper\SecureHtmlRenderer; /** @@ -125,6 +125,9 @@ protected function _prepareAttributes($title, $classes, $disabled) 'value' => $this->getValue(), 'disabled' => $disabled, ]; + if ($this->hasData('onclick_attribute')) { + $attributes['onclick'] = $this->getData('onclick_attribute'); + } if ($this->hasData('backend_button_widget_hook_id')) { $attributes['backend-button-widget-hook-id'] = $this->getData('backend_button_widget_hook_id'); } diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertLinkActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertLinkActionGroup.xml new file mode 100644 index 0000000000000..6fa63d14b9612 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertLinkActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertLinkActionGroup"> + <annotations> + <description>Assert text and url of the links.</description> + </annotations> + <arguments> + <argument name="text" type="string"/> + <argument name="url" type="string"/> + </arguments> + + <seeLink userInput="{{text}}" url="{{url}}" stepKey="assertLinks"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml index b2b71c4ad3eca..ab5f18780082b 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml @@ -22,6 +22,8 @@ <after> <!-- 6. Restore default configuration settings. --> <magentoCLI command="config:set {{DefaultWebCookieLifetimeConfigData.path}} {{DefaultWebCookieLifetimeConfigData.value}}" stepKey="setDefaultCookieLifetime"/> + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <!-- Delete data --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml index b0fbdb8b5b596..8b0bf1dc963ff 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml @@ -23,70 +23,91 @@ <!-- Logging in Magento admin and checking for Privacy policy footer in dashboard --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <closeAdminNotification stepKey="closeAdminNotification"/> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkDashboard"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkDashboard"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in salesOrderPage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSalesOrder"> <argument name="menuUiId" value="magento-sales-sales"/> <argument name="submenuUiId" value="magento-sales-sales-order"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkSalesOrder"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkSalesOrder"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in catalogProductsPage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCatalogProducts"> <argument name="menuUiId" value="magento-catalog-catalog"/> <argument name="submenuUiId" value="magento-catalog-catalog-products"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkCatalogProducts"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkCatalogProducts"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in customersAllCustomersPage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToCustomersAllCustomers"> <argument name="menuUiId" value="magento-customer-customer"/> <argument name="submenuUiId" value="magento-customer-customer-manage"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkCustomersAllCustomers"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkCustomersAllCustomers"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in marketingCatalogPriceRulePage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingCatalogPriceRule"> <argument name="menuUiId" value="magento-backend-marketing"/> <argument name="submenuUiId" value="magento-catalogrule-promo-catalog"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkMarketingCatalogPriceRule"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkMarketingCatalogPriceRule"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in contentBlocksPage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentBlocks"> <argument name="menuUiId" value="magento-backend-content"/> <argument name="submenuUiId" value="magento-cms-cms-block"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkContentBlocks"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkContentBlocks"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in reportSearcbTermsPage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToReportsSearchTerms"> <argument name="menuUiId" value="magento-reports-report"/> <argument name="submenuUiId" value="magento-search-report-search-term"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkReportsSearchTerms"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkReportsSearchTerms"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in storesAllStoresPage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresAllStores"> <argument name="menuUiId" value="magento-backend-stores"/> <argument name="submenuUiId" value="magento-backend-system-store"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkStoresAllStores"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkStoresAllStores"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in systemImportPage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSystemImport"> <argument name="menuUiId" value="magento-backend-system"/> <argument name="submenuUiId" value="magento-importexport-system-convert-import"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkSystemImport"/> - + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkSystemImport"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> <!-- Checking for Privacy policy footer in findPartnersAndExtensionsPage --> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToFindPartnersAndExtensions"> <argument name="menuUiId" value="magento-marketplace-partners"/> <argument name="submenuUiId" value="magento-marketplace-partners"/> </actionGroup> - <seeLink userInput="Privacy Policy" url="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf" stepKey="seePrivacyPolicyLinkFindPartnersAndExtensions"/> + <actionGroup ref="AssertLinkActionGroup" stepKey="seePrivacyPolicyLinkFindPartnersAndExtensions"> + <argument name="text" value="Privacy Policy"/> + <argument name="url" value="https://magento.com/sites/default/files/REVISED-MAGENTO-PRIVACY-POLICY.pdf"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php index be14a51ffb27b..33667f158b9d9 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/ButtonTest.php @@ -94,4 +94,16 @@ public function getAttributesHtmlDataProvider() ] ]; } + + /** + * Verifies ability of adding button onclick attribute + * + * @return void + */ + public function testOnClickAttribute(): void + { + $this->_blockMock->setData(['onclick_attribute' => 'value']); + $attributes = $this->_blockMock->getAttributesHtml(); + $this->assertStringContainsString('onclick', $attributes); + } } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection.php index ead687faff7bc..45018406277f9 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection.php @@ -145,7 +145,8 @@ public function getParentIdsByChild($childId) ['e.entity_id as parent_product_id'] )->where( $this->getMainTable() . '.product_id IN(?)', - $childId + $childId, + \Zend_Db::INT_TYPE ); return $connection->fetchCol($select); diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml index 26119c5267d86..39d026ac74731 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml @@ -59,6 +59,18 @@ </actionGroup> <checkOption selector="{{AdminAddProductsToOptionPanel.firstCheckbox}}" stepKey="selectFirstGridRow2"/> <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> + <!-- Check that Bundle Options initialized with default quantity --> + <grabValueFrom selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" stepKey="grabbedFirstBundleOptionQuantity"/> + <assertEquals stepKey="assertFirstBundleOptionDefaultQuantity"> + <expectedResult type="string">1</expectedResult> + <actualResult type="string">$grabbedFirstBundleOptionQuantity</actualResult> + </assertEquals> + <grabValueFrom selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" stepKey="grabbedSecondBundleOptionQuantity"/> + <assertEquals stepKey="assertSecondBundleOptionDefaultQuantity"> + <expectedResult type="string">1</expectedResult> + <actualResult type="string">$grabbedSecondBundleOptionQuantity</actualResult> + </assertEquals> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty1"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillProductDefaultQty2"/> @@ -108,6 +120,17 @@ </actionGroup> <checkOption selector="{{AdminProductFormBundleSection.firstProductOption}}" stepKey="selectNewFirstGridRow2"/> <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddNewSelectedBundleProducts"/> + <!-- Check that existing Bundle Options do not loose user input quantity values --> + <grabValueFrom selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" stepKey="grabbedFirstBundleOptionQuantityAfterUserInput"/> + <assertEquals stepKey="assertFirstBundleOptionDefaultQuantityAfterUserInput"> + <expectedResult type="string">{{BundleProduct.defaultQuantity}}</expectedResult> + <actualResult type="string">$grabbedFirstBundleOptionQuantityAfterUserInput</actualResult> + </assertEquals> + <grabValueFrom selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" stepKey="grabbedSecondBundleOptionQuantityAfterUserInput"/> + <assertEquals stepKey="assertSecondBundleOptionDefaultQuantityAfterUserInput"> + <expectedResult type="string">{{BundleProduct.defaultQuantity}}</expectedResult> + <actualResult type="string">$grabbedSecondBundleOptionQuantityAfterUserInput</actualResult> + </assertEquals> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '2')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillNewProductDefaultQty1"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '3')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillNewProductDefaultQty2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml index f73941c375a41..8818fadc1d10c 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml @@ -42,10 +42,7 @@ <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <!-- Reindex --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml index 8b50fffec091f..6924e389451cd 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml @@ -36,9 +36,7 @@ <requiredEntity createDataKey="createSimpleProduct"/> </createData> <!-- TODO: Remove this action when MC-37719 will be fixed --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexInvalidatedIndices"> - <argument name="indices" value="cataloginventory_stock"/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexInvalidatedIndices"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml index f8f98384ee8da..0474de1144f4e 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml @@ -133,9 +133,7 @@ <click selector="{{AdminProductFiltersSection.enable}}" stepKey="ClickOnEnable"/> <!--Clear Cache - reindex - resets products according to enabled/disabled view--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml index 93fac3171e9fb..88f992e698181 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml @@ -41,7 +41,7 @@ <requiredEntity createDataKey="apiSimple"/> </createData> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <argument name="indices" value="cataloginventory_stock catalog_product_price"/> </actionGroup> </before> <after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml index fe4faed29d144..8b19753067593 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml @@ -48,8 +48,7 @@ <argument name="productId" value="$createFixedBundleProduct.id$"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <!--Perform reindex and flush cache--> - <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + <comment userInput="Adding the comment to replace AdminReindexAndFlushCache action group ('indexer:reindex', 'cache:flush' commands) for preserving Backward Compatibility" stepKey="reindexAndFlushCache"/> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProductForBundleItem"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml index 5997cdc14ade8..f7bce778cc0d8 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml @@ -39,8 +39,9 @@ <requiredEntity createDataKey="fixedBundleOption"/> <requiredEntity createDataKey="createSimpleProductTwo"/> </createData> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <argument name="indices" value="cataloginventory_stock catalog_product_price"/> </actionGroup> </before> <after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml index 7049299987dff..d127f18355643 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml @@ -96,7 +96,7 @@ <!-- Perform CLI reindex --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <argument name="indices" value="cataloginventory_stock catalog_product_price"/> </actionGroup> </before> <after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml index 24c481c9ddcb2..927cebdf7e508 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml @@ -168,7 +168,7 @@ <see userInput="You saved the configuration." selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccess"/> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <argument name="indices" value="cataloginventory_stock catalog_product_price"/> </actionGroup> </before> <after> diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-option-qty.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-option-qty.js index e61def6e962a4..5904b20a5dabe 100644 --- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-option-qty.js +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-option-qty.js @@ -17,6 +17,26 @@ define([ } }, + /** + * @inheritdoc + */ + setInitialValue: function () { + this.initialValue = this.getInitialValue(); + + if (this.initialValue === undefined || this.initialValue === '') { + this.initialValue = 1; + } + + if (this.value.peek() !== this.initialValue) { + this.value(this.initialValue); + } + + this.on('value', this.onUpdate.bind(this)); + this.isUseDefault(this.disabled()); + + return this; + }, + /** * @inheritdoc */ @@ -33,6 +53,5 @@ define([ return !this.visible() ? false : notEqual; } - }); }); diff --git a/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php b/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php index 5cdfdc88e7dc1..fdd69cc268f97 100644 --- a/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php +++ b/app/code/Magento/BundleGraphQl/Model/Cart/BundleOptionDataProvider.php @@ -9,6 +9,8 @@ use Magento\Bundle\Helper\Catalog\Product\Configuration; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item; use Magento\Framework\Pricing\Helper\Data; use Magento\Framework\Serialize\SerializerInterface; @@ -18,6 +20,11 @@ */ class BundleOptionDataProvider { + /** + * Option type name + */ + private const OPTION_TYPE = 'bundle'; + /** * @var Data */ @@ -33,19 +40,26 @@ class BundleOptionDataProvider */ private $configuration; + /** @var Uid */ + private $uidEncoder; + /** * @param Data $pricingHelper * @param SerializerInterface $serializer * @param Configuration $configuration + * @param Uid|null $uidEncoder */ public function __construct( Data $pricingHelper, SerializerInterface $serializer, - Configuration $configuration + Configuration $configuration, + Uid $uidEncoder = null ) { $this->pricingHelper = $pricingHelper; $this->serializer = $serializer; $this->configuration = $configuration; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -103,6 +117,7 @@ private function buildBundleOptions(array $bundleOptions, Item $item): array $options[] = [ 'id' => $bundleOption->getId(), + 'uid' => $this->uidEncoder->encode(self::OPTION_TYPE . '/' . $bundleOption->getId()), 'label' => $bundleOption->getTitle(), 'type' => $bundleOption->getType(), 'values' => $this->buildBundleOptionValues($bundleOption->getSelections(), $item), @@ -131,9 +146,15 @@ private function buildBundleOptionValues(array $selections, Item $item): array } $selectionPrice = $this->configuration->getSelectionFinalPrice($item, $selection); - + $optionDetails = [ + self::OPTION_TYPE, + $selection->getData('option_id'), + $selection->getData('selection_id'), + (int) $selection->getData('selection_qty') + ]; $values[] = [ 'id' => $selection->getSelectionId(), + 'uid' => $this->uidEncoder->encode(implode('/', $optionDetails)), 'label' => $selection->getName(), 'quantity' => $qty, 'price' => $this->pricingHelper->currency($selectionPrice, false, false), diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php index 8025cf91d28c9..0f8cdc27d2417 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php @@ -10,7 +10,9 @@ use Magento\Bundle\Model\Selection; use Magento\Bundle\Model\ResourceModel\Selection\CollectionFactory; use Magento\Bundle\Model\ResourceModel\Selection\Collection as LinkCollection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Query\EnumLookup; +use Magento\Framework\GraphQl\Query\Uid; /** * Collection to fetch link data at resolution time. @@ -42,14 +44,23 @@ class Collection */ private $links = []; + /** @var Uid */ + private $uidEncoder; + /** * @param CollectionFactory $linkCollectionFactory * @param EnumLookup $enumLookup + * @param Uid|null $uidEncoder */ - public function __construct(CollectionFactory $linkCollectionFactory, EnumLookup $enumLookup) - { + public function __construct( + CollectionFactory $linkCollectionFactory, + EnumLookup $enumLookup, + Uid $uidEncoder = null + ) { $this->linkCollectionFactory = $linkCollectionFactory; $this->enumLookup = $enumLookup; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -117,6 +128,7 @@ private function fetch() : array 'price' => $link->getSelectionPriceValue(), 'position' => $link->getPosition(), 'id' => $link->getSelectionId(), + 'uid' => $this->uidEncoder->encode((string) $link->getSelectionId()), 'qty' => (float)$link->getSelectionQty(), 'quantity' => (float)$link->getSelectionQty(), 'is_default' => (bool)$link->getIsDefault(), diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php index ce5c12ce69675..c08d69a887089 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** @@ -23,6 +24,17 @@ class BundleItemOptionUid implements ResolverInterface */ private const OPTION_TYPE = 'bundle'; + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + /** * Create a option uid for entered option in "<option-type>/<option-id>/<option-value-id>/<quantity>" format * @@ -62,7 +74,6 @@ public function resolve( $content = implode('/', $optionDetails); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - return base64_encode($content); + return $this->uidEncoder->encode($content); } } diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php index c8e2384fcb99c..fe1b47bc635b6 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php @@ -5,11 +5,12 @@ */ declare(strict_types=1); - namespace Magento\BundleGraphQl\Model\Resolver\Options; use Magento\Bundle\Model\OptionFactory; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Store\Model\StoreManagerInterface; /** @@ -17,6 +18,11 @@ */ class Collection { + /** + * Option type name + */ + private const OPTION_TYPE = 'bundle'; + /** * @var OptionFactory */ @@ -42,19 +48,26 @@ class Collection */ private $optionMap = []; + /** @var Uid */ + private $uidEncoder; + /** * @param OptionFactory $bundleOptionFactory * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param StoreManagerInterface $storeManager + * @param Uid|null $uidEncoder */ public function __construct( OptionFactory $bundleOptionFactory, JoinProcessorInterface $extensionAttributesJoinProcessor, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + Uid $uidEncoder = null ) { $this->bundleOptionFactory = $bundleOptionFactory; $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; $this->storeManager = $storeManager; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -101,7 +114,7 @@ private function fetch() : array $linkField = $optionsCollection->getConnection()->getAutoIncrementField($productTable); $optionsCollection->getSelect()->join( ['cpe' => $productTable], - 'cpe.'.$linkField.' = main_table.parent_id', + 'cpe.' . $linkField . ' = main_table.parent_id', [] )->where( "cpe.entity_id IN (?)", @@ -124,6 +137,8 @@ private function fetch() : array = $option->getTitle() === null ? $option->getDefaultTitle() : $option->getTitle(); $this->optionMap[$option->getParentId()][$option->getId()]['sku'] = $this->skuMap[$option->getParentId()]['sku']; + $this->optionMap[$option->getParentId()][$option->getId()]['uid'] + = $this->uidEncoder->encode(self::OPTION_TYPE . '/' . $option->getOptionId()); } return $this->optionMap; diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php index a21bbbb84d735..9bbe69c95f552 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Serialize\Serializer\Json; use Magento\Sales\Api\Data\InvoiceItemInterface; @@ -23,6 +24,11 @@ */ class BundleOptions implements ResolverInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'bundle'; + /** * Serializer * @@ -35,16 +41,22 @@ class BundleOptions implements ResolverInterface */ private $valueFactory; + /** @var Uid */ + private $uidEncoder; + /** * @param ValueFactory $valueFactory * @param Json $serializer + * @param Uid $uidEncoder */ public function __construct( ValueFactory $valueFactory, - Json $serializer + Json $serializer, + Uid $uidEncoder ) { $this->valueFactory = $valueFactory; $this->serializer = $serializer; + $this->uidEncoder = $uidEncoder; } /** @@ -89,7 +101,9 @@ private function getBundleOptions( foreach ($options['bundle_options'] ?? [] as $bundleOptionId => $bundleOption) { $bundleOptions[$bundleOptionId]['label'] = $bundleOption['label'] ?? ''; $bundleOptions[$bundleOptionId]['id'] = isset($bundleOption['option_id']) ? - base64_encode($bundleOption['option_id']) : null; + $this->uidEncoder->encode((string) $bundleOption['option_id']) : null; + $bundleOptions[$bundleOptionId]['uid'] = isset($bundleOption['option_id']) ? + $this->uidEncoder->encode(self::OPTION_TYPE . '/' . $bundleOption['option_id']) : null; if (isset($bundleOption['option_id'])) { $bundleOptions[$bundleOptionId]['values'] = $this->formatBundleOptionItems( $item, @@ -127,8 +141,20 @@ private function formatBundleOptionItems( // Value Id is missing from parent, so we have to match the child to parent option if (isset($bundleChildAttributes['option_id']) && $bundleChildAttributes['option_id'] == $bundleOptionId) { + + $options = $childOrderItemOptions['info_buyRequest'] + ['bundle_option'][$bundleChildAttributes['option_id']]; + + $optionDetails = [ + self::OPTION_TYPE, + $bundleChildAttributes['option_id'], + implode(',', $options), + (int) $childOrderItemOptions['info_buyRequest']['qty'] + ]; + $optionItems[$childrenOrderItem->getItemId()] = [ - 'id' => base64_encode($childrenOrderItem->getItemId()), + 'id' => $this->uidEncoder->encode((string) $childrenOrderItem->getItemId()), + 'uid' => $this->uidEncoder->encode(implode('/', $optionDetails)), 'product_name' => $childrenOrderItem->getName(), 'product_sku' => $childrenOrderItem->getSku(), 'quantity' => $bundleChildAttributes['qty'], diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls index a2cba24c7c4d4..8a60eb671b0b6 100644 --- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls @@ -32,21 +32,24 @@ type BundleCartItem implements CartItemInterface { } type SelectedBundleOption { - id: Int! + id: Int! @deprecated(reason: "Use `uid` instead") + uid: ID! @doc(description: "The unique ID for a `SelectedBundleOption` object") label: String! type: String! values: [SelectedBundleOptionValue!]! } type SelectedBundleOptionValue { - id: Int! + id: Int! @doc(description: "Use `uid` instead") + uid: ID! @doc(description: "The unique ID for a `SelectedBundleOptionValue` object") label: String! quantity: Float! price: Float! } type BundleItem @doc(description: "BundleItem defines an individual item in a bundle product.") { - option_id: Int @doc(description: "An ID assigned to each type of item in a bundle product.") + option_id: Int @deprecated(reason: "Use `uid` instead") @doc(description: "An ID assigned to each type of item in a bundle product.") + uid: ID @doc(description: "The unique ID for a `BundleItem` object.") title: String @doc(description: "The display name of the item.") required: Boolean @doc(description: "Indicates whether the item must be included in the bundle.") type: String @doc(description: "The input type that the customer uses to select the item. Examples include radio button and checkbox.") @@ -56,7 +59,7 @@ type BundleItem @doc(description: "BundleItem defines an individual item in a bu } type BundleItemOption @doc(description: "BundleItemOption defines characteristics and options for a specific bundle item.") { - id: Int @doc(description: "The ID assigned to the bundled item option.") + id: Int @deprecated(reason: "Use `uid` instead") @doc(description: "The ID assigned to the bundled item option.") label: String @doc(description: "The text that identifies the bundled item option.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\Label") qty: Float @deprecated(reason: "The `qty` is deprecated. Use `quantity` instead.") @doc(description: "Indicates the quantity of this specific bundle item.") quantity: Float @doc(description: "Indicates the quantity of this specific bundle item.") @@ -66,7 +69,7 @@ type BundleItemOption @doc(description: "BundleItemOption defines characteristic price_type: PriceTypeEnum @doc(description: "One of FIXED, PERCENT, or DYNAMIC.") can_change_quantity: Boolean @doc(description: "Indicates whether the customer can change the number of items for this option.") product: ProductInterface @doc(description: "Contains details about this product option.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\BundleItemOptionUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `BundleItemOption` object.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\BundleItemOptionUid") } type BundleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "BundleProduct defines basic features of a bundle product and contains multiple BundleItems.") { @@ -105,13 +108,15 @@ type BundleCreditMemoItem implements CreditMemoItemInterface { } type ItemSelectedBundleOption @doc(description: "A list of options of the selected bundle product") { - id: ID! @doc(description: "The unique identifier of the option") + id: ID! @deprecated(reason: "Use `uid` instead") @doc(description: "The unique ID for a `ItemSelectedBundleOption` object") + uid: ID! @doc(description: "The unique ID for a `ItemSelectedBundleOption` object") label: String! @doc(description: "The label of the option") values: [ItemSelectedBundleOptionValue] @doc(description: "A list of products that represent the values of the parent option") } type ItemSelectedBundleOptionValue @doc(description: "A list of values for the selected bundle product") { - id: ID! @doc(description: "The unique identifier of the value") + id: ID! @deprecated(reason: "Use `uid` instead") @doc(description: "The unique ID for a `ItemSelectedBundleOptionValue` object") + uid: ID! @doc(description: "The unique ID for a `ItemSelectedBundleOptionValue` object") product_name: String! @doc(description: "The name of the child bundle product") product_sku: String! @doc(description: "The SKU of the child bundle product") quantity: Float! @doc(description: "Indicates how many of this bundle product were ordered") diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml b/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml index 515c2bc56f067..6ddce634ec957 100644 --- a/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml +++ b/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml @@ -42,7 +42,7 @@ <argument name="tags" value="full_page"/> </actionGroup> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> - <argument name="indices" value=""/> + <argument name="indices" value="catalog_product_price"/> </actionGroup> <!-- Check Bundle product is visible on the storefront--> @@ -63,9 +63,7 @@ <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterUpdate"> <argument name="tags" value="full_page"/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterUpdate"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="indexerReindexAfterUpdate"/> <!-- Check Bundle product is still visible on the storefront--> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPageAfterUpdate"> diff --git a/app/code/Magento/Captcha/Helper/Data.php b/app/code/Magento/Captcha/Helper/Data.php index 9a14f9ae9a21d..8a9131a499b13 100644 --- a/app/code/Magento/Captcha/Helper/Data.php +++ b/app/code/Magento/Captcha/Helper/Data.php @@ -150,7 +150,8 @@ public function getFonts() */ public function getImgDir($website = null) { - $mediaDir = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA); + // Captcha images are not re-used and should be stored only locally. + $mediaDir = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA, Filesystem\DriverPool::FILE); $captchaDir = '/captcha/' . $this->_getWebsiteCode($website); $mediaDir->create($captchaDir); return $mediaDir->getAbsolutePath($captchaDir) . '/'; diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index 57cea59bee207..b06edc43cd71d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -209,7 +209,14 @@ public function getImagesJson() */ private function sortImagesByPosition($images) { - if (is_array($images)) { + $nullPositions = []; + foreach ($images as $index => $image) { + if ($image['position'] === null) { + $nullPositions[] = $image; + unset($images[$index]); + } + } + if (is_array($images) && !empty($images)) { usort( $images, function ($imageA, $imageB) { @@ -217,7 +224,7 @@ function ($imageA, $imageB) { } ); } - return $images; + return array_merge($images, $nullPositions); } /** diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php index 3a9d81eed4221..af921959f8e27 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/Type/Date.php @@ -5,6 +5,11 @@ */ namespace Magento\Catalog\Block\Product\View\Options\Type; +use DateTimeZone; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\FilterFactory; +use Magento\Framework\Stdlib\DateTime; + /** * Product options text type block * @@ -27,22 +32,30 @@ class Date extends \Magento\Catalog\Block\Product\View\Options\AbstractOptions */ protected $_catalogProductOptionTypeDate; + /** + * @var FilterFactory + */ + private $filterFactory; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper * @param \Magento\Catalog\Helper\Data $catalogData * @param \Magento\Catalog\Model\Product\Option\Type\Date $catalogProductOptionTypeDate * @param array $data + * @param FilterFactory|null $filterFactory */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Pricing\Helper\Data $pricingHelper, \Magento\Catalog\Helper\Data $catalogData, \Magento\Catalog\Model\Product\Option\Type\Date $catalogProductOptionTypeDate, - array $data = [] + array $data = [], + ?FilterFactory $filterFactory = null ) { $this->_catalogProductOptionTypeDate = $catalogProductOptionTypeDate; parent::__construct($context, $pricingHelper, $catalogData, $data); + $this->filterFactory = $filterFactory ?? ObjectManager::getInstance()->get(FilterFactory::class); } /** @@ -77,14 +90,24 @@ public function getDateHtml() public function getCalendarDateHtml() { $option = $this->getOption(); - $value = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId() . '/date'); + $values = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); $yearStart = $this->_catalogProductOptionTypeDate->getYearStart(); $yearEnd = $this->_catalogProductOptionTypeDate->getYearEnd(); - $dateFormat = $this->_localeDate->getDateFormat(\IntlDateFormatter::SHORT); + $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); /** Escape RTL characters which are present in some locales and corrupt formatting */ $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); + $value = null; + if (is_array($values)) { + $date = $this->getInternalDateString($values); + if ($date !== null) { + $dateFilter = $this->filterFactory->create('date', ['format' => $escapedDateFormat]); + $value = $dateFilter->outputFilter($date); + } elseif (isset($values['date'])) { + $value = $values['date']; + } + } $calendar = $this->getLayout()->createBlock( \Magento\Framework\View\Element\Html\Date::class )->setId( @@ -158,8 +181,8 @@ public function getTimeHtml() * Return drop-down html with range of values * * @param string $name Id/name of html select element - * @param int $from Start position - * @param int $to End position + * @param int $from Start position + * @param int $to End position * @param int|null $value Value selected * @return string Formatted Html */ @@ -209,9 +232,8 @@ protected function _getHtmlSelect($name, $value = null) $select->setExtraParams($extraParams); if ($value === null) { - $value = $this->getProduct()->getPreconfiguredValues()->getData( - 'options/' . $option->getId() . '/' . $name - ); + $values = $this->getProduct()->getPreconfiguredValues()->getData('options/' . $option->getId()); + $value = is_array($values) ? $this->parseDate($values, $name) : null; } if ($value !== null) { $select->setValue($value); @@ -233,4 +255,56 @@ protected function _getValueWithLeadingZeros($value) } return $value < 10 ? '0' . $value : $value; } + + /** + * Get internal date format of provided value + * + * @param array $value + * @return string|null + */ + private function getInternalDateString(array $value): ?string + { + $result = null; + if (!empty($value['date']) && !empty($value['date_internal'])) { + $dateTimeZone = new DateTimeZone($this->_localeDate->getConfigTimezone()); + $dateTimeObject = date_create_from_format( + DateTime::DATETIME_PHP_FORMAT, + $value['date_internal'], + $dateTimeZone + ); + if ($dateTimeObject !== false) { + $result = $dateTimeObject->format(DateTime::DATE_PHP_FORMAT); + } + } elseif (!empty($value['day']) && !empty($value['month']) && !empty($value['year'])) { + $dateTimeObject = $this->_localeDate->date(); + $dateTimeObject->setDate((int) $value['year'], (int) $value['month'], (int) $value['day']); + $result = $dateTimeObject->format(DateTime::DATE_PHP_FORMAT); + } + return $result; + } + + /** + * Parse option value and return the requested part + * + * @param array $value + * @param string $part [year, month, day, hour, minute, day_part] + * @return string|null + */ + private function parseDate(array $value, string $part): ?string + { + $result = null; + if (!empty($value['date']) && !empty($value['date_internal'])) { + $formatDate = explode(' ', $value['date_internal']); + $date = explode('-', $formatDate[0]); + $value['year'] = $date[0]; + $value['month'] = $date[1]; + $value['day'] = $date[2]; + } + + if (isset($value[$part])) { + $result = (string) $value[$part]; + } + + return $result; + } } diff --git a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php index f5d56dc9e6b0e..3f94ffd0909aa 100644 --- a/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php +++ b/app/code/Magento/Catalog/Controller/Product/Compare/Remove.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Product\Compare; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\Exception\NoSuchEntityException; diff --git a/app/code/Magento/Catalog/Model/CompareList.php b/app/code/Magento/Catalog/Model/CompareList.php new file mode 100644 index 0000000000000..5be30d40aacce --- /dev/null +++ b/app/code/Magento/Catalog/Model/CompareList.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Framework\Model\AbstractModel; + +class CompareList extends AbstractModel +{ + /** + * Initialize resource + * + * @return void + */ + protected function _construct() + { + $this->_init(ResourceModel\Product\Compare\CompareList::class); + } +} diff --git a/app/code/Magento/Catalog/Model/CompareListIdToMaskedListId.php b/app/code/Magento/Catalog/Model/CompareListIdToMaskedListId.php new file mode 100644 index 0000000000000..a911980b98894 --- /dev/null +++ b/app/code/Magento/Catalog/Model/CompareListIdToMaskedListId.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Model\ResourceModel\Product\Compare\CompareList as CompareListResource; +use Magento\Framework\Exception\LocalizedException; + +/** + * CompareListId to MaskedListId resolver + */ +class CompareListIdToMaskedListId +{ + /** + * @var CompareListFactory + */ + private $compareListFactory; + + /** + * @var CompareListResource + */ + private $compareListResource; + + /** + * @param CompareListFactory $compareListFactory + * @param CompareListResource $compareListResource + */ + public function __construct( + CompareListFactory $compareListFactory, + CompareListResource $compareListResource + ) { + $this->compareListFactory = $compareListFactory; + $this->compareListResource = $compareListResource; + } + + /** + * Get listIdMask by listId + * + * @param int $listId + * + * @param int|null $customerId + * @return null|string + * @throws LocalizedException + */ + public function execute(int $listId, int $customerId = null): ?string + { + $compareList = $this->compareListFactory->create(); + $this->compareListResource->load($compareList, $listId, 'list_id'); + if ((int)$compareList->getCustomerId() !== (int)$customerId) { + throw new LocalizedException(__('This customer is not authorized to access this list')); + } + return $compareList->getListIdMask() ?? null; + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index 38f606b8abefe..020c19578f75d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -430,7 +430,7 @@ protected function prepareSelectsByRange( $field, $select, $range, - \Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + \Magento\Framework\DB\Query\BatchIteratorInterface::UNIQUE_FIELD_ITERATOR ); $queries = []; diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php index f3a4b322e29df..404fd27232b93 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -465,10 +465,11 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) [] )->where( 'e.entity_id IN(?)', - $parentIds + $parentIds, + \Zend_Db::INT_TYPE ); if (!empty($excludeIds)) { - $select->where('child_id NOT IN(?)', $excludeIds); + $select->where('child_id NOT IN(?)', $excludeIds, \Zend_Db::INT_TYPE); } $children = $this->getConnection()->fetchCol($select); @@ -479,7 +480,8 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) $this->getIndexTargetTableByDimension($dimensions) )->where( 'entity_id IN(?)', - $children + $children, + \Zend_Db::INT_TYPE ); $query = $select->insertFromSelect($this->_defaultIndexerResource->getIdxTable(), [], false); $this->getConnection()->query($query); @@ -578,13 +580,14 @@ private function getParentProductsTypes(array $productsIds) ['e.entity_id as parent_id', 'type_id'] )->where( 'l.child_id IN(?)', - $productsIds + $productsIds, + \Zend_Db::INT_TYPE ); $pairs = $this->getConnection()->fetchPairs($select); $byType = []; foreach ($pairs as $productId => $productType) { - $byType[$productType][$productId] = $productId; + $byType[$productType][$productId] = (int)$productId; } return $byType; diff --git a/app/code/Magento/Catalog/Model/MaskedListIdToCompareListId.php b/app/code/Magento/Catalog/Model/MaskedListIdToCompareListId.php new file mode 100644 index 0000000000000..cd1506c970763 --- /dev/null +++ b/app/code/Magento/Catalog/Model/MaskedListIdToCompareListId.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Model\ResourceModel\Product\Compare\CompareList as CompareListResource; +use Magento\Framework\Exception\LocalizedException; + +/** + * MaskedListId to ListId resolver + */ +class MaskedListIdToCompareListId +{ + /** + * @var CompareListFactory + */ + private $compareListFactory; + + /** + * @var CompareListResource + */ + private $compareListResource; + + /** + * @param CompareListFactory $compareListFactory + * @param CompareListResource $compareListResource + */ + public function __construct( + CompareListFactory $compareListFactory, + CompareListResource $compareListResource + ) { + $this->compareListFactory = $compareListFactory; + $this->compareListResource = $compareListResource; + } + + /** + * Get maskedId by listId + * + * @param string $maskedListId + * @param int $customerId + * @return int + * @throws LocalizedException + */ + public function execute(string $maskedListId, int $customerId = null): int + { + $compareList = $this->compareListFactory->create(); + $this->compareListResource->load($compareList, $maskedListId, 'list_id_mask'); + if ((int)$compareList->getCustomerId() !== (int)$customerId) { + throw new LocalizedException(__('This customer is not authorized to access this list')); + } + return (int)$compareList->getListId(); + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index 5a9d53ce80cf8..7a1bd21d78182 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -267,29 +267,50 @@ protected function processNewAndExistingImages($product, array &$images) { foreach ($images as &$image) { if (empty($image['removed'])) { + $isNew = empty($image['value_id']); $data = $this->processNewImage($product, $image); - if (!$product->isObjectNew()) { - $this->resourceModel->deleteGalleryValueInStore( - $image['value_id'], - $product->getData($this->metadata->getLinkField()), - $product->getStoreId() - ); - } // Add per store labels, position, disabled $data['value_id'] = $image['value_id']; $data['label'] = isset($image['label']) ? $image['label'] : ''; - $data['position'] = isset($image['position']) ? (int)$image['position'] : 0; + $data['position'] = isset($image['position']) && $image['position'] !== '' + ? (int)$image['position'] + : null; $data['disabled'] = isset($image['disabled']) ? (int)$image['disabled'] : 0; $data['store_id'] = (int)$product->getStoreId(); $data[$this->metadata->getLinkField()] = (int)$product->getData($this->metadata->getLinkField()); - $this->resourceModel->insertGalleryValueInStore($data); + $this->saveGalleryStoreValue($product, $data); + if ($isNew && $data['store_id'] !== Store::DEFAULT_STORE_ID) { + $dataForDefaultScope = $data; + $dataForDefaultScope['store_id'] = Store::DEFAULT_STORE_ID; + $dataForDefaultScope['disabled'] = 0; + $dataForDefaultScope['label'] = null; + $this->saveGalleryStoreValue($product, $dataForDefaultScope); + } } } } + /** + * Save media gallery store value + * + * @param Product $product + * @param array $data + */ + private function saveGalleryStoreValue(Product $product, array $data): void + { + if (!$product->isObjectNew()) { + $this->resourceModel->deleteGalleryValueInStore( + $data['value_id'], + $data[$this->metadata->getLinkField()], + $data['store_id'] + ); + } + $this->resourceModel->insertGalleryValueInStore($data); + } + /** * Processes image as new. * diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php index a3726207b3024..ed2e09249e495 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/ReadHandler.php @@ -64,9 +64,9 @@ public function execute($entity, $arguments = []) $this->addMediaDataToProduct( $entity, - $mediaEntries + $this->sortMediaEntriesByPosition($mediaEntries) ); - + return $entity; } @@ -108,7 +108,7 @@ public function getAttribute() * Find default value * * @param string $key - * @param string[] &$image + * @param string[] $image * @return string * @deprecated 101.0.1 * @since 101.0.0 @@ -121,4 +121,30 @@ protected function findDefaultValue($key, &$image) return ''; } + + /** + * Sort media entries by position + * + * @param array $mediaEntries + * @return array + */ + private function sortMediaEntriesByPosition(array $mediaEntries): array + { + $mediaEntriesWithNullPositions = []; + foreach ($mediaEntries as $index => $mediaEntry) { + if ($mediaEntry['position'] === null) { + $mediaEntriesWithNullPositions[] = $mediaEntry; + unset($mediaEntries[$index]); + } + } + if (!empty($mediaEntries)) { + usort( + $mediaEntries, + function ($entryA, $entryB) { + return ($entryA['position'] < $entryB['position']) ? -1 : 1; + } + ); + } + return array_merge($mediaEntries, $mediaEntriesWithNullPositions); + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php index 77ef8ef4853e1..c85c2e47548a0 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php @@ -6,10 +6,11 @@ namespace Magento\Catalog\Model\Product\Option\Type; +use Magento\Catalog\Model\Product\Exception as ProductException; +use Magento\Catalog\Helper\Product as ProductHelper; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Exception\LocalizedException; -use Magento\Catalog\Model\Product\Exception as ProductException; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\App\ObjectManager; @@ -91,6 +92,11 @@ class File extends \Magento\Catalog\Model\Product\Option\Type\DefaultType */ private $filesystem; + /** + * @var ProductHelper + */ + private $productHelper; + /** * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -103,6 +109,7 @@ class File extends \Magento\Catalog\Model\Product\Option\Type\DefaultType * @param array $data * @param Filesystem $filesystem * @param Json|null $serializer + * @param ProductHelper|null $productHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -116,7 +123,8 @@ public function __construct( \Magento\Framework\Escaper $escaper, array $data = [], Filesystem $filesystem = null, - Json $serializer = null + Json $serializer = null, + ProductHelper $productHelper = null ) { $this->_itemOptionFactory = $itemOptionFactory; $this->_urlBuilder = $urlBuilder; @@ -129,6 +137,7 @@ public function __construct( $this->validatorInfo = $validatorInfo; $this->validatorFile = $validatorFile; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $this->productHelper = $productHelper ?: ObjectManager::getInstance()->get(ProductHelper::class); parent::__construct($checkoutSession, $scopeConfig, $data); } @@ -223,12 +232,21 @@ public function validateUserValue($values) $this->setIsValid(true); $option = $this->getOption(); + if (isset($values['files_prefix'])) { + $processingParams = ['files_prefix' => $values['files_prefix']]; + $processingParams = array_merge($this->_getProcessingParams()->getData(), $processingParams); + $this->productHelper->addParamsToBuyRequest($this->getRequest(), $processingParams); + } + /* * Check whether we receive uploaded file or restore file by: reorder/edit configuration or * previous configuration with no newly uploaded file */ $fileInfo = null; - if (isset($values[$option->getId()]) && is_array($values[$option->getId()])) { + if (isset($values[$option->getId()])) { + if (is_string($values[$option->getId()])) { + $values[$option->getId()] = explode(',', $values[$option->getId()]); + } // Legacy style, file info comes in array with option id index $fileInfo = $values[$option->getId()]; } else { diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index 74a6c7f634f81..206f3dba0ee60 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -379,7 +379,7 @@ public function getTierPrices($product) if (array_key_exists('website_price', $price)) { $value = $price['website_price']; } else { - $value = $price['price']; + $value = $price['price'] ?? 0; } $tierPrice->setValue($value); $tierPrice->setQty($price['price_qty']); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php index 203126cf1fd8c..03f1edea0ea77 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php @@ -101,7 +101,7 @@ protected function _clearUselessAttributeValues(\Magento\Framework\Model\Abstrac $attributeStoreIds = array_keys($this->_storeManager->getStores()); if (!empty($attributeStoreIds)) { $delCondition = [ - 'attribute_id = ?' => $object->getId(), + 'attribute_id = ?' => (int)$object->getId(), 'store_id IN(?)' => $attributeStoreIds, ]; $this->getConnection()->delete($object->getBackendTable(), $delCondition); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index e19286efc38c0..ed2df0f10ac3b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -575,7 +575,8 @@ public function verifyIds(array $ids) 'entity_id' )->where( 'entity_id IN(?)', - $ids + $ids, + \Zend_Db::INT_TYPE ); return $this->getConnection()->fetchCol($select); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/CompareList.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/CompareList.php new file mode 100644 index 0000000000000..4185df079d55d --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/CompareList.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Product\Compare; + +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; + +/** + * Compare List resource class + */ +class CompareList extends AbstractDb +{ + /** + * @inheritdoc + */ + protected function _construct() + { + $this->_init('catalog_compare_list', 'list_id'); + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php index 7eb0552e355fc..ff29a5afa7eda 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item.php @@ -45,6 +45,10 @@ public function loadByProduct(\Magento\Catalog\Model\Product\Compare\Item $objec $select->where('visitor_id = ?', (int)$object->getVisitorId()); } + if ($object->getListId()) { + $select->where('list_id = ?', (int)$object->getListId()); + } + $data = $connection->fetchRow($select); if (!$data) { @@ -140,6 +144,7 @@ public function purgeVisitorByCustomer($object) /** * Update (Merge) customer data from visitor + * * After Login process * * @param \Magento\Catalog\Model\Product\Compare\Item $object diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php index 92741cf9ba88e..76f566a364769 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php @@ -31,6 +31,13 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ protected $_visitorId = 0; + /** + * List Id Filter + * + * @var int + */ + protected $listId = 0; + /** * Comparable attributes cache * @@ -156,6 +163,30 @@ public function setCustomerId($customerId) return $this; } + /** + * Set listId filter to collection + * + * @param int $listId + * + * @return $this + */ + public function setListId(int $listId) + { + $this->listId = $listId; + $this->_addJoinToSelect(); + return $this; + } + + /** + * Retrieve listId filter applied to collection + * + * @return int + */ + public function getListId(): int + { + return (int)$this->listId; + } + /** * Set visitor filter to collection * @@ -204,6 +235,10 @@ public function getConditionForJoin() return ['visitor_id' => $this->getVisitorId()]; } + if ($this->getListId()) { + return ['list_id' => $this->getListId()]; + } + return ['customer_id' => ['null' => true], 'visitor_id' => '0']; } @@ -232,6 +267,82 @@ public function _addJoinToSelect() return $this; } + /** + * Get products ids by for compare list + * + * @param int $listId + * + * @return array + */ + public function getProductsByListId(int $listId): array + { + $select = $this->getConnection()->select()-> + from( + $this->getTable('catalog_compare_item'), + 'product_id' + )->where( + 'list_id = ?', + $listId + ); + return $this->getConnection()->fetchCol($select); + } + + + /** + * Set list_id for customer compare item + * + * @param int $listId + * @param int $customerId + */ + public function setListIdToCustomerCompareItems(int $listId, int $customerId) + { + foreach ($this->getCustomerCompareItems($customerId) as $itemId) { + $this->getConnection()->update( + $this->getTable('catalog_compare_item'), + ['list_id' => $listId], + ['catalog_compare_item_id = ?' => (int)$itemId] + ); + } + } + + /** + * Remove compare list if customer compare list empty + * + * @param int|null $customerId + */ + public function removeCompareList(?int $customerId) + { + if (empty($this->getCustomerCompareItems($customerId))) { + $this->getConnection()->delete( + $this->getTable('catalog_compare_list'), + ['customer_id = ?' => $customerId] + ); + } + } + + /** + * Get customer compare items + * + * @param int|null $customerId + * @return array + */ + private function getCustomerCompareItems(?int $customerId): array + { + if ($customerId) { + $select = $this->getConnection()->select()-> + from( + $this->getTable('catalog_compare_item') + )->where( + 'customer_id = ?', + $customerId + ); + + return $this->getConnection()->fetchCol($select); + } + + return []; + } + /** * Retrieve comapre products attribute set ids * diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php index 392a4aeedfeb3..76584ea2a65f3 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php @@ -149,12 +149,19 @@ public function getRelationsByChildren(array $childrenIds): array $select = $connection->select() ->from( ['cpe' => $this->getTable('catalog_product_entity')], - 'entity_id' + ['relation.child_id', 'cpe.entity_id'] )->join( ['relation' => $this->getTable('catalog_product_relation')], 'relation.parent_id = cpe.' . $linkField )->where('relation.child_id IN(?)', $childrenIds); - return $connection->fetchCol($select); + $result = $connection->fetchAll($select); + $parentIdsOfChildIds = []; + + foreach ($result as $row) { + $parentIdsOfChildIds[$row['child_id']][] = $row['entity_id']; + } + + return $parentIdsOfChildIds; } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Url.php b/app/code/Magento/Catalog/Model/ResourceModel/Url.php index be95f088a2477..eceae322fbd8e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Url.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Url.php @@ -204,7 +204,8 @@ protected function _getCategoryAttribute($attributeCode, $categoryIds, $storeId) ['value' => $attributeCode, 'entity_id' => 'entity_id'] )->where( 'entity_id IN(?)', - $categoryIds + $categoryIds, + \Zend_Db::INT_TYPE ); } elseif ($this->_categoryAttributes[$attributeCode]['is_global'] || $storeId == 0) { $select->from( @@ -216,7 +217,8 @@ protected function _getCategoryAttribute($attributeCode, $categoryIds, $storeId) ['value'] )->where( "t1.{$identifierFiled} IN(?)", - $categoryIds + $categoryIds, + \Zend_Db::INT_TYPE )->where( 'e.attribute_id = :attribute_id' )->where( @@ -245,7 +247,8 @@ protected function _getCategoryAttribute($attributeCode, $categoryIds, $storeId) 't1.attribute_id = :attribute_id' )->where( "e.entity_id IN(?)", - $categoryIds + $categoryIds, + \Zend_Db::INT_TYPE )->group('e.entity_id'); $bind['attribute_id'] = $this->_categoryAttributes[$attributeCode]['attribute_id']; @@ -308,7 +311,8 @@ public function _getProductAttribute($attributeCode, $productIds, $storeId) 0 )->where( 'entity_id IN(?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); } else { $valueExpr = $connection->getCheckSql('t2.value_id > 0', 't2.value', 't1.value'); @@ -326,7 +330,8 @@ public function _getProductAttribute($attributeCode, $productIds, $storeId) 't1.attribute_id = :attribute_id' )->where( 't1.entity_id IN(?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); $bind['store_id'] = $storeId; } @@ -430,7 +435,7 @@ protected function _getCategories($categoryIds, $storeId = null, $path = null) // Prepare variables for checking whether categories belong to store if ($path === null) { - $select->where('main_table.entity_id IN(?)', $categoryIds); + $select->where('main_table.entity_id IN(?)', $categoryIds, \Zend_Db::INT_TYPE); } else { // Ensure that path ends with '/', otherwise we can get wrong results - e.g. $path = '1/2' will get '1/20' if (substr($path, -1) != '/') { @@ -569,7 +574,7 @@ protected function _getProducts($productIds, $storeId, $entityId, &$lastEntityId $this->_productLimit ); if ($productIds !== null) { - $select->where('e.entity_id IN(?)', $productIds); + $select->where('e.entity_id IN(?)', $productIds, \Zend_Db::INT_TYPE); } $rowSet = $connection->fetchAll($select, $bind); @@ -591,7 +596,8 @@ protected function _getProducts($productIds, $storeId, $entityId, &$lastEntityId ['product_id', 'category_id'] )->where( 'product_id IN(?)', - array_keys($products) + array_keys($products), + \Zend_Db::INT_TYPE ); $categories = $connection->fetchAll($select); foreach ($categories as $category) { diff --git a/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php index ca87efaa87490..bdee84762cac2 100644 --- a/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php +++ b/app/code/Magento/Catalog/Observer/CategoryProductIndexer.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Observer; use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Catalog\Model\Indexer\Category\Flat\State as FlatState; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; @@ -21,12 +22,21 @@ class CategoryProductIndexer implements ObserverInterface */ private $processor; + /** + * @var FlatState + */ + private $flatState; + /** * @param Processor $processor + * @param FlatState $flatState */ - public function __construct(Processor $processor) - { + public function __construct( + Processor $processor, + FlatState $flatState + ) { $this->processor = $processor; + $this->flatState = $flatState; } /** @@ -35,7 +45,7 @@ public function __construct(Processor $processor) public function execute(Observer $observer): void { $productIds = $observer->getEvent()->getProductIds(); - if (!empty($productIds) && $this->processor->isIndexerScheduled()) { + if (!empty($productIds) && $this->processor->isIndexerScheduled() && $this->flatState->isFlatEnabled()) { $this->processor->markIndexerAsInvalid(); } } diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignTwoCategoriesToProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignTwoCategoriesToProductActionGroup.xml new file mode 100644 index 0000000000000..06b4b68630326 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssignTwoCategoriesToProductActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssignTwoCategoriesToProductActionGroup" extends="AdminAssignCategoryToProductAndSaveActionGroup"> + <annotations> + <description>Extends AdminAssignCategoryToProductAndSaveActionGroup + assigns the second category and prevents product saving (the Product Edit page should be opened in Admin prior this check).</description> + </annotations> + <arguments> + <argument name="categoryTwoName" type="string"/> + </arguments> + <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="openDropDown2" after="waitForApplyCategory"/> + <checkOption selector="{{AdminProductFormSection.selectCategory(categoryTwoName)}}" stepKey="selectCategoryTwo"/> + <click selector="{{AdminProductFormSection.done}}" stepKey="clickDone2"/> + <waitForPageLoad stepKey="waitForApplyCategoryTwo"/> + <remove keyForRemoval="clickSave"/> + <remove keyForRemoval="waitForSavingProduct"/> + <remove keyForRemoval="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckNameToggleOnProductsMassAttributeUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckNameToggleOnProductsMassAttributeUpdateActionGroup.xml new file mode 100644 index 0000000000000..919e1bcb0dcb2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckNameToggleOnProductsMassAttributeUpdateActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCheckNameToggleOnProductsMassAttributesUpdateActionGroup"> + <annotations> + <description>Click the "Change" checkbox for the "Name" field on the Products Masss Attributes Update page.</description> + </annotations> + + <click selector="{{AdminEditProductAttributesSection.ChangeAttributeNameToggle}}" stepKey="toggleToChangeName"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductOnProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductOnProductGridActionGroup.xml new file mode 100644 index 0000000000000..64fab5575e392 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductOnProductGridActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCheckProductOnProductGridActionGroup"> + <annotations> + <description>Check the checkbox for the product on the Product Grid</description> + </annotations> + <arguments> + <argument name="product" type="entity"/> + </arguments> + + <checkOption selector="{{AdminProductGridSection.productRowCheckboxBySku(product.sku)}}" stepKey="selectProduct"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml index 90cc7666eb92f..ec3d26e8a3f36 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml @@ -14,6 +14,7 @@ </annotations> <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> - <seeInCurrentUrl url="catalog/product_action_attribute/edit/" stepKey="seeInUrl"/> + <waitForPageLoad stepKey="waitForBulkUpdatePage"/> + <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeInUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickSaveOnProductsMassAttributeUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickSaveOnProductsMassAttributeUpdateActionGroup.xml new file mode 100644 index 0000000000000..a3328c5eb115b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickSaveOnProductsMassAttributeUpdateActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminClickSaveOnProductsMassAttributesUpdateActionGroup"> + <annotations> + <description>Clicks on 'Save' button on products mass attributes update page.</description> + </annotations> + + <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> + <waitForPageLoad stepKey="waitForUpdateAttributesPage"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Update Attributes" stepKey="seeUpdateAttributesTitle"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml new file mode 100644 index 0000000000000..fe5b0ae1a64ce --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteAllProductAttributesFilteredByCodeActionGroup.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteAllProductAttributesFilteredByCodeActionGroup"> + <annotations> + <description>Open product attributes grid filter it by attribute code and delete all found attributes one by one.</description> + </annotations> + <arguments> + <argument name="codeFilter" type="string" defaultValue="fake-code"/> + </arguments> + + <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="clearExistingFilters"/> + <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{codeFilter}}" stepKey="fillAttributeCodeFilterField"/> + <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="applyGridFilter"/> + <helper class="\Magento\Catalog\Test\Mftf\Helper\CatalogHelper" method="deleteAllProductAttributesOneByOne" stepKey="deleteAllProductAttributesOneByOne"> + <argument name="notEmptyRow">{{AdminDataGridTableSection.firstNotEmptyRow2}}</argument> + <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> + <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> + <argument name="successMessageContainer">{{AdminMessagesSection.success}}</argument> + <argument name="successMessage">You deleted the product attribute.</argument> + </helper> + <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="clearExistingFiltersAgain"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillMainProductFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillMainProductFormActionGroup.xml new file mode 100644 index 0000000000000..c5818a4eea51b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminFillMainProductFormActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillMainProductFormActionGroup" extends="FillMainProductFormActionGroup"> + <annotations> + <description>Extends FillMainProductFormActionGroup with filling the next fields: Tax Class, Visibility, SEO->URL </description> + </annotations> + + <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{product.productTaxClass}}" stepKey="selectProductTaxClass"/> + <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{product.visibility}}" stepKey="selectVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> + <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{product.urlKey}}" stepKey="fillUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyIncrementsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyIncrementsActionGroup.xml new file mode 100644 index 0000000000000..a39ac8c3f5d2b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyIncrementsActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMassUpdateProductQtyIncrementsActionGroup"> + <arguments> + <argument name="enableQtyIncrements" type="string" defaultValue="Yes"/> + <argument name="qtyIncrements" type="string" defaultValue="2"/> + </arguments> + <click selector="{{AdminUpdateAttributesAdvancedInventorySection.inventory}}" stepKey="openInventoryTab"/> + <checkOption selector="{{AdminUpdateAttributesAdvancedInventorySection.changeEnableQtyIncrements}}" stepKey="changeEnableQtyIncrements"/> + <uncheckOption selector="{{AdminUpdateAttributesAdvancedInventorySection.useConfigEnableQtyIncrements}}" stepKey="uncheckUseConfigEnableQtyIncrements"/> + <selectOption selector="{{AdminUpdateAttributesAdvancedInventorySection.enableQtyIncrements}}" userInput="{{enableQtyIncrements}}" stepKey="setEnableQtyIncrements"/> + <checkOption selector="{{AdminUpdateAttributesAdvancedInventorySection.changeQtyIncrements}}" stepKey="changeQtyIncrements"/> + <uncheckOption selector="{{AdminUpdateAttributesAdvancedInventorySection.useConfigQtyIncrements}}" stepKey="uncheckUseConfigQtyIncrements"/> + <fillField selector="{{AdminUpdateAttributesAdvancedInventorySection.qtyIncrements}}" userInput="{{qtyIncrements}}" stepKey="setQtyIncrements"/> + <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="save"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitVisibleSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSearchGridByStringNoClearActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSearchGridByStringNoClearActionGroup.xml new file mode 100644 index 0000000000000..afaa3c28c56e8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSearchGridByStringNoClearActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSearchGridByStringNoClearActionGroup"> + <annotations> + <description>Search the Admin grid by string without clearing filters.</description> + </annotations> + <arguments> + <argument name="keyword" defaultValue="" type="string"/> + </arguments> + + <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{keyword}}" stepKey="fillKeywordSearchField"/> + <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearch"/> + <waitForPageLoad stepKey="waitForProductSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusActionGroup.xml new file mode 100644 index 0000000000000..05f5a90cc97a7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetStockStatusActionGroup"> + <annotations> + <description>Set Stock Status of product.</description> + </annotations> + + <arguments> + <argument name="stockStatus" type="string" defaultValue="In Stock"/> + </arguments> + + <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{stockStatus}}" stepKey="setStockStatus"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminManageStockOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminManageStockOnEditPageActionGroup.xml new file mode 100644 index 0000000000000..eca989a5de41f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminManageStockOnEditPageActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminManageStockOnEditPageActionGroup"> + <annotations> + <description>Check if manageStock value is correct + (the Product Edit page->Advanced Inventory section should be opened in Admin prior this check).</description> + </annotations> + <arguments> + <argument name="manageStock" type="string"/> + </arguments> + + <see selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="{{manageStock}}" stepKey="seeManageStock"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductInfoOnEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductInfoOnEditPageActionGroup.xml new file mode 100644 index 0000000000000..d91cdfee0489c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductInfoOnEditPageActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminProductInfoOnEditPageActionGroup"> + <annotations> + <description>Validates next fields on the Product Edit Page: + name, sku, price, quantity, stock status, tax class, weight, weigh select, visibility, url key</description> + </annotations> + <arguments> + <argument name="product" type="entity"/> + </arguments> + <waitForPageLoad stepKey="waitForProductToLoad"/> + <seeInField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="seeProductName"/> + <seeInField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="seeProductSku"/> + <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{product.price}}" stepKey="seeProductPrice"/> + <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{product.quantity}}" stepKey="seeProductQuantity"/> + <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{product.status}}" stepKey="seeProductStockStatus"/> + <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{product.productTaxClass}}" stepKey="seeProductTaxClass"/> + <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{product.weight}}" stepKey="seeSimpleProductWeight"/> + <seeInField selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{product.weightSelect}}" stepKey="seeSimpleProductWeightSelect"/> + <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{product.visibility}}" stepKey="seeVisibility"/> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> + <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{product.urlKey}}" stepKey="seeUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductIsAssignedToCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductIsAssignedToCategoryActionGroup.xml new file mode 100644 index 0000000000000..e6b178937a2cc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminProductIsAssignedToCategoryActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminProductIsAssignedToCategoryActionGroup"> + <annotations> + <description>Checks if product is assigned to category (the Product Edit page should be opened in Admin prior this check).</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <seeElement selector="{{AdminProductFormSection.categories(categoryName)}}" stepKey="seeCategoryName"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryChangingFullscreenImageByRibbonActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryChangingFullscreenImageByRibbonActionGroup.xml new file mode 100644 index 0000000000000..6423bf5e319b7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryChangingFullscreenImageByRibbonActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductPageGalleryChangingFullscreenImageByRibbonActionGroup"> + <annotations> + <description>On the product page change main image by clicking on the images in the ribbon. Fullscreen</description> + </annotations> + <arguments> + <argument name="startImage" type="string" /> + <argument name="expectedImage" type="string" /> + </arguments> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageFullscreen(startImage)}}" stepKey="seeStartFullscreenImage"/> + <seeElement selector="{{StorefrontProductMediaSection.imgSelectedInThumbnail(startImage)}}" stepKey="seeActiveImageInThumbnail"/> + <click selector="{{StorefrontProductMediaSection.productImageInFotorama(expectedImage)}}" stepKey="clickOnExpectedImage"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(expectedImage)}}" stepKey="seeExpectedImageAfterClick"/> + <seeElement selector="{{StorefrontProductMediaSection.imgSelectedInThumbnail(expectedImage)}}" stepKey="seeExpectedImageActiveInThumbnailAfterChange"/> + <click selector="{{StorefrontProductMediaSection.productImageInFotorama(startImage)}}" stepKey="clickOnStartImageInRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(startImage)}}" stepKey="seeStartImageAfterSecondChange"/> + <seeElement selector="{{StorefrontProductMediaSection.imgSelectedInThumbnail(startImage)}}" stepKey="seeStartImageActiveInThumbnailAfterSecondChange"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryChangingMainImageByRibbonActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryChangingMainImageByRibbonActionGroup.xml new file mode 100644 index 0000000000000..b196956135043 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryChangingMainImageByRibbonActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductPageGalleryChangingMainImageByRibbonActionGroup"> + <annotations> + <description>Changing main image on product page media gallery by clicking on the images in the fotorama ribbon</description> + </annotations> + <arguments> + <argument name="startImage" type="string" /> + <argument name="expectedImage" type="string" /> + </arguments> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImage(startImage)}}" stepKey="waitActiveImageDefault"/> + <seeElement selector="{{StorefrontProductMediaSection.imgSelectedInThumbnail(startImage)}}" stepKey="seeActiveImageThumbnail"/> + <click selector="{{StorefrontProductMediaSection.productImageInFotorama(expectedImage)}}" stepKey="firstClickOnImageInRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.imgSelectedInThumbnail(expectedImage)}}" stepKey="seeExpectedImageSelectedInRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(expectedImage)}}" stepKey="seeChangedImageAfterFirstClick"/> + <click selector="{{StorefrontProductMediaSection.productImageInFotorama(startImage)}}" stepKey="secondClickOnImageInRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.imgSelectedInThumbnail(startImage)}}" stepKey="seeStartImageSelectedInRibbonAfterSecondClick"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(startImage)}}" stepKey="seeChangedImageAfterSecondClick"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryFullscreenThumbnailDragActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryFullscreenThumbnailDragActionGroup.xml new file mode 100644 index 0000000000000..86803aed4cfb6 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryFullscreenThumbnailDragActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductPageGalleryFullscreenThumbnailDragActionGroup"> + <annotations> + <description>On the product page check functional of drag actions in the fotorama ribbon during fullscreen</description> + </annotations> + <arguments> + <argument name="dragPointImage" type="string" /> + <argument name="currentImage" type="string"/> + <argument name="firstImage" type="string" /> + </arguments> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImageFullscreen(currentImage)}}" stepKey="seeFullscreenImage"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(firstImage)}}" stepKey="seeFirstImageInRibbon"/> + <dragAndDrop selector1="{{StorefrontProductMediaSection.productImageInFotorama(dragPointImage)}}" selector2="{{StorefrontProductMediaSection.productImageInFotorama(dragPointImage)}}" x="-300" y="0" stepKey="dragRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(currentImage)}}" stepKey="seeFullscreenImageAfterDrag"/> + <dontSeeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(firstImage)}}" stepKey="dontSeeFirstImageInRibbonAfterDrag"/> + <dragAndDrop selector1="{{StorefrontProductMediaSection.productImageInFotorama(dragPointImage)}}" selector2="{{StorefrontProductMediaSection.productImageInFotorama(dragPointImage)}}" x="300" y="0" stepKey="dragBackRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(currentImage)}}" stepKey="seeMainImageAfterBackDrag"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(firstImage)}}" stepKey="seeFirstImageInRibbonAfterBackDrag"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryImageDimensionsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryImageDimensionsActionGroup.xml new file mode 100644 index 0000000000000..7b6a8e14455ca --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryImageDimensionsActionGroup.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductPageGalleryImageDimensionsActionGroup"> + <annotations> + <description>On the product page grab dimensions of the displayed product image, and image section. Assert that image is less or equals</description> + </annotations> + <arguments> + <argument name="imageSource" defaultValue="{{StorefrontProductMediaSection.mainImageForJsActions}}" type="string"/> + </arguments> + <executeJS function="var img=document.querySelector('{{imageSource}}'); + return img.clientHeight;" stepKey="getImageHeight"/> + <executeJS function="var img=document.querySelector('{{imageSource}}'); + return img.clientWidth;" stepKey="getImageWidth"/> + <executeJS function="var img=document.querySelector('{{StorefrontProductMediaSection.imageSectionForJsActions}}'); + return img.clientHeight;" stepKey="getSectionHeight"/> + <executeJS function="var img=document.querySelector('{{StorefrontProductMediaSection.imageSectionForJsActions}}'); + return img.clientWidth;" stepKey="getSectionWidth"/> + <assertLessThanOrEqual stepKey="checkHeightIsCorrect"> + <actualResult type="variable">getImageHeight</actualResult> + <expectedResult type="variable">getSectionHeight</expectedResult> + </assertLessThanOrEqual> + <assertLessThanOrEqual stepKey="checkWidthIsCorrect"> + <actualResult type="variable">getImageWidth</actualResult> + <expectedResult type="variable">getSectionWidth</expectedResult> + </assertLessThanOrEqual> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup.xml new file mode 100644 index 0000000000000..6473f348648f2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup"> + <annotations> + <description>Check the expected image position in the fotorama ribbon on the product page</description> + </annotations> + <arguments> + <argument name="image" defaultValue="Magento2.filename" type="string"/> + <argument name="extension" defaultValue="Magento2.file_extension" type="string"/> + <argument name="position" defaultValue="0" type="string" /> + </arguments> + <grabAttributeFrom userInput="src" selector="{{StorefrontProductMediaSection.fotoramaImageThumbnailImgByNumber(position)}}" stepKey="grabSrcFromThumbnailImageByPosition"/> + <assertRegExp stepKey="checkImagePositionInThumbnail"> + <actualResult type="variable">$grabSrcFromThumbnailImageByPosition</actualResult> + <expectedResult type="string">|{{image}}[_\d]*.{{extension}}|</expectedResult> + </assertRegExp> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryMainImageButtonsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryMainImageButtonsActionGroup.xml new file mode 100644 index 0000000000000..d4cc3097946be --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryMainImageButtonsActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductPageGalleryMainImageButtonsActionGroup"> + <annotations> + <description>Assert the buttons functionality "change image" on the product media gallery on the product page</description> + </annotations> + <arguments> + <argument name="startImage" type="string" /> + <argument name="expectedImage" type="string" /> + </arguments> + <moveMouseOver selector="{{StorefrontProductMediaSection.mainImageForJsActions}}" stepKey="hoverOverImage"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="waitForButtons"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(startImage)}}" stepKey="seeProductImageBeforeActions"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButton"/> + <seeElement selector="{{StorefrontProductMediaSection.imgSelectedInThumbnail(expectedImage)}}" stepKey="seeExpectedImageSelectedInRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(expectedImage)}}" stepKey="seeExpectedImageOnPreview"/> + <click selector="{{StorefrontProductMediaSection.imagePrevButton}}" stepKey="clickOnPrevImageButton"/> + <seeElement selector="{{StorefrontProductMediaSection.imgSelectedInThumbnail(startImage)}}" stepKey="seeActiveImageSelectedInRibbonAfterSecondChange"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(startImage)}}" stepKey="seeMainProductImageAfterSecondChange"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryThumbnailDragActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryThumbnailDragActionGroup.xml new file mode 100644 index 0000000000000..2e62d973ea090 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductPageGalleryThumbnailDragActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductPageGalleryThumbnailDragActionGroup"> + <annotations> + <description>Check functional of drag actions in the thumbnail ribbon on the product page</description> + </annotations> + <arguments> + <argument name="dragPointImage" type="string" /> + <argument name="currentImage" type="string"/> + <argument name="firstImage" type="string" /> + </arguments> + <waitForElementVisible selector="{{StorefrontProductMediaSection.productImage(currentImage)}}" stepKey="seeMainImageBeforeDragActions"/> + <dontSeeElement selector="{{StorefrontProductMediaSection.fotoramaPrevButton}}" stepKey="dontSeePrevButtonBeforeDragActions"/> + <dragAndDrop selector1="{{StorefrontProductMediaSection.productImageInFotorama(dragPointImage)}}" selector2="{{StorefrontProductMediaSection.productImageInFotorama(dragPointImage)}}" x="-300" y="0" stepKey="dragRibbonForward"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(currentImage)}}" stepKey="seeMainImageDontChangeAfterDrag"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.fotoramaPrevButton}}" stepKey="waitPrevButton"/> + <seeElement selector="{{StorefrontProductMediaSection.fotoramaPrevButton}}" stepKey="seePrevButton"/> + <dontSeeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(firstImage)}}" stepKey="dontSeeFirstImageInRibbonAfterDrag"/> + <dragAndDrop selector1="{{StorefrontProductMediaSection.productImageInFotorama(dragPointImage)}}" selector2="{{StorefrontProductMediaSection.productImageInFotorama(dragPointImage)}}" x="300" y="0" stepKey="dragBackRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(currentImage)}}" stepKey="seeMainImageDontChangeAfterBackDrag"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(firstImage)}}" stepKey="seeFirstImageInRibbonAfterBackDrag"/> + <dontSeeElement selector="{{StorefrontProductMediaSection.fotoramaPrevButton}}" stepKey="dontSeePrevButtonAfterBackDrag"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductStockStatusOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductStockStatusOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..857d88ebc197c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontProductStockStatusOnProductPageActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontProductStockStatusOnProductPageActionGroup"> + <annotations> + <description>Validates that the provided Product Stock Status is present and correct + (the Product Detail page should be opened on Storefront prior this check)</description> + </annotations> + <arguments> + <argument name="productStockStatus" type="string"/> + </arguments> + + <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="{{productStockStatus}}" stepKey="seeProductStockStatus"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageGalleryDragMainImageBackActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageGalleryDragMainImageBackActionGroup.xml new file mode 100644 index 0000000000000..4925d6627a0b3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageGalleryDragMainImageBackActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontProductPageGalleryDragMainImageBackActionGroup"> + <annotations> + <description>Drag back main image in the media gallery of product page</description> + </annotations> + <moveMouseOver selector="{{StorefrontProductMediaSection.mainImageForJsActions}}" stepKey="hoverOnProductImage"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="waitNextButton"/> + <dragAndDrop selector1="{{StorefrontProductMediaSection.mainImageForJsActions}}" selector2="{{StorefrontProductMediaSection.imageNextButton}}" x="200" y="0" stepKey="dragImageBack"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageGalleryDragMainImageForwardActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageGalleryDragMainImageForwardActionGroup.xml new file mode 100644 index 0000000000000..a75a25e31717b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontProductPageGalleryDragMainImageForwardActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontProductPageGalleryDragMainImageForwardActionGroup"> + <annotations> + <description>Drag forward main image in the media gallery of product page</description> + </annotations> + <moveMouseOver selector="{{StorefrontProductMediaSection.mainImageForJsActions}}" stepKey="hoverOnProductImage"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="waitNextButton"/> + <dragAndDrop selector1="{{StorefrontProductMediaSection.mainImageForJsActions}}" selector2="{{StorefrontProductMediaSection.imagePrevButton}}" x="-366" y="0" stepKey="dragImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml index a2391dda54809..e1072001b56e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml @@ -25,4 +25,52 @@ <data key="name">adobe-thumb</data> <data key="extension">jpg</data> </entity> + <entity name="AdobeSmallImage" type="image"> + <data key="title" unique="suffix">magento-small</data> + <data key="file">adobe-small.jpg</data> + <data key="filename">adobe-small</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="AdobeThumbImage" type="image"> + <data key="title" unique="suffix">magento-thumb</data> + <data key="file">adobe-thumb.jpg</data> + <data key="filename">adobe-thumb</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="JpgImage" type="image"> + <data key="title" unique="suffix">jpgimage</data> + <data key="file">jpg.jpg</data> + <data key="filename">jpg</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="LargeImage" type="image"> + <data key="title" unique="suffix">largeimage</data> + <data key="file">large.jpg</data> + <data key="filename">large</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="MagentoImage" type="image"> + <data key="title" unique="suffix">magentoimage</data> + <data key="file">magento.jpg</data> + <data key="filename">magento</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="MagentoStage" type="image"> + <data key="title" unique="suffix">magentostage</data> + <data key="file">magentoStage.jpg</data> + <data key="filename">magentoStage</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="MediumImage" type="image"> + <data key="title" unique="suffix">mediumimage</data> + <data key="file">medium.jpg</data> + <data key="filename">medium</data> + <data key="file_extension">jpg</data> + </entity> + <entity name="PngImage" type="image"> + <data key="title" unique="suffix">magentoimage</data> + <data key="file">png.png</data> + <data key="filename">png</data> + <data key="file_extension">png</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index e7760a9b90f0c..5375459122e69 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -370,6 +370,7 @@ <data key="shareable">Yes</data> <data key="file">magento-logo.png</data> <data key="fileName">magento-logo</data> + <data key="file_extension">png</data> </entity> <entity name="MagentoLogo" type="image"> <data key="title" unique="suffix">MagentoLogo</data> @@ -585,6 +586,10 @@ <var key="sku" entityType="product" entityKey="sku" /> <requiredEntity type="product_option">ProductOptionValueDropdown</requiredEntity> </entity> + <entity name="productWithFileOption" type="product"> + <var key="sku" entityType="product" entityKey="sku" /> + <requiredEntity type="product_option">ProductOptionFile</requiredEntity> + </entity> <entity name="productWithDropdownAndFieldOptions" type="product"> <var key="sku" entityType="product" entityKey="sku" /> <requiredEntity type="product_option">ProductOptionValueDropdown</requiredEntity> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml index 731754ef01959..b763fda489b20 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -80,4 +80,10 @@ <data key="quantity">1</data> <var key="sku" entityType="product" entityKey="sku" /> </entity> + <entity name="tierPrice01PercentDiscount" type="data"> + <data key="website">All Websites [USD]</data> + <data key="customer_group">ALL GROUPS</data> + <data key="price">0.1</data> + <data key="qty">1</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml index 7bb8cf5f4db37..3965cfa1f958b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/WYSIWYGConfigData.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> - +<!--TODO: This datasets should be moved to CMS module--> <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="EnableWYSIWYG"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Helper/CatalogHelper.php b/app/code/Magento/Catalog/Test/Mftf/Helper/CatalogHelper.php new file mode 100644 index 0000000000000..dcba3b1bf68de --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Helper/CatalogHelper.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Mftf\Helper; + +use Facebook\WebDriver\Remote\RemoteWebDriver as FacebookWebDriver; +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; + +/** + * Class for MFTF helpers for Catalog module. + */ +class CatalogHelper extends Helper +{ + /** + * Delete all product attributes one by one. + * + * @param string $notEmptyRow + * @param string $modalAcceptButton + * @param string $deleteButton + * @param string $successMessageContainer + * @param string $successMessage + * @retrun void + */ + public function deleteAllProductAttributesOneByOne( + string $notEmptyRow, + string $modalAcceptButton, + string $deleteButton, + string $successMessageContainer, + string $successMessage + ): void { + try { + /** @var MagentoWebDriver $webDriver */ + $magentoWebDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + /** @var FacebookWebDriver $webDriver */ + $webDriver = $magentoWebDriver->webDriver; + $gridRows = $webDriver->findElements(WebDriverBy::cssSelector($notEmptyRow)); + while (!empty($gridRows)) { + $gridRows[0]->click(); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForElementVisible($successMessageContainer); + $magentoWebDriver->see($successMessage, $successMessageContainer); + $gridRows = $webDriver->findElements(WebDriverBy::cssSelector($notEmptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml index 1ca051e2f6669..d70c48f2b00e3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml @@ -77,5 +77,6 @@ <element name="newAddedAttribute" type="text" selector="//fieldset[@class='admin__fieldset']//div[contains(@data-index,'{{attributeCode}}')]" parameterized="true"/> <element name="newCategoryButton" type="button" selector="button[data-index='create_category_button']" timeout="30"/> <element name="footerBlock" type="block" selector="//footer"/> + <element name="categories" type="text" selector="//*[@class='admin__action-multiselect-crumb']/span[contains(text(), '{{categoryName}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml index 92dadbdd26c2d..9881545fac9cd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml @@ -13,5 +13,11 @@ <element name="qty" type="input" selector="#inventory_qty"/> <element name="changeStockAvailability" type="checkbox" selector="#inventory_stock_availability_checkbox"/> <element name="stockAvailability" type="select" selector="//select[@name='inventory[is_in_stock]']"/> + <element name="enableQtyIncrements" type="select" selector="#inventory_enable_qty_increments"/> + <element name="useConfigEnableQtyIncrements" type="checkbox" selector="#inventory_use_config_enable_qty_increments"/> + <element name="changeEnableQtyIncrements" type="checkbox" selector="#inventory_enable_qty_increments_checkbox"/> + <element name="qtyIncrements" type="input" selector="#inventory_qty_increments"/> + <element name="useConfigQtyIncrements" type="checkbox" selector="#inventory_use_config_qty_increments"/> + <element name="changeQtyIncrements" type="checkbox" selector="#inventory_qty_increments_checkbox"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml index 13ced1c0263e0..64f365217d7e4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml @@ -14,5 +14,6 @@ <element name="addToCartButtonTitleIsAdding" type="text" selector="//button/span[text()='Adding...']"/> <element name="addToCartButtonTitleIsAdded" type="text" selector="//button/span[text()='Added']"/> <element name="addToCartButtonTitleIsAddToCart" type="text" selector="//button/span[text()='Add to Cart']"/> + <element name="inputFormKey" type="text" selector="input[name='form_key']"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml index 447113ea65bb2..5efa094e2c35e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductMediaSection.xml @@ -16,11 +16,21 @@ <element name="closeFullscreenImage" type="button" selector="//*[@data-gallery-role='gallery' and contains(@class, 'fullscreen')]//*[@data-gallery-role='fotorama__fullscreen-icon']" /> <element name="imageFile" type="text" selector="//*[@class='product media']//img[contains(@src, '{{filename}}')]" parameterized="true"/> <element name="productImageActive" type="text" selector=".product.media div[data-active=true] > img[src*='{{filename}}']" parameterized="true"/> - <element name="productImageInFotorama" type="file" selector=".fotorama__nav__shaft img[src*='{{imageName}}']" parameterized="true"/> - <element name="fotoramaPrevButton" type="button" selector="//*[@data-gallery-role='gallery']//*[@data-gallery-role='nav-wrap']//*[@data-gallery-role='arrow' and contains(@class, 'fotorama__thumb__arr--left')]"/> - <element name="fotoramaNextButton" type="button" selector="//*[@data-gallery-role='gallery']//*[@data-gallery-role='nav-wrap']//*[@data-gallery-role='arrow' and contains(@class, 'fotorama__thumb__arr--right')]"/> + <element name="productImageInFotorama" type="file" selector=".fotorama__nav__shaft img[src*='{{imageName}}']" parameterized="true" timeout="30"/> + <element name="fotoramaPrevButton" type="button" selector="//*[@data-gallery-role='gallery']//*[@data-gallery-role='nav-wrap']//*[@data-gallery-role='arrow' and contains(@class, 'fotorama__thumb__arr--left')]" timeout="30"/> + <element name="fotoramaNextButton" type="button" selector="//*[@data-gallery-role='gallery']//*[@data-gallery-role='nav-wrap']//*[@data-gallery-role='arrow' and contains(@class, 'fotorama__thumb__arr--right')]" timeout="30"/> + <element name="fotoramaNextButtonVideo" type="button" selector="div.fotorama__arr.fotorama__arr--next.fotorama__arr--shown" timeout="30"/> <element name="fotoramaAnyMedia" type="text" selector=".fotorama__nav__shaft img"/> <element name="fotoramaImageThumbnail" type="block" selector="//div[contains(@class, 'fotorama__nav__shaft')]//div[contains(@class, 'fotorama__nav__frame--thumb')][{{imageNumber}}]" parameterized="true" timeout="30"/> + <element name="fotoramaImageThumbnailImgByNumber" type="block" selector="//div[contains(@class, 'fotorama__nav__shaft')]//div[contains(@class, 'fotorama__nav__frame--thumb')][{{imageNumber}}]//img" parameterized="true" timeout="30"/> <element name="fotoramaImageThumbnailActive" type="block" selector="//div[contains(@class, 'fotorama__nav__shaft')]//div[contains(@class, 'fotorama__nav__frame--thumb') and contains(@class, 'fotorama__active')][{{imageNumber}}]" parameterized="true" timeout="30"/> + <element name="imageNextButton" type="button" selector=".product.media .fotorama-item .fotorama__wrap--toggle-arrows .fotorama__arr--next" timeout="30"/> + <element name="imageFullscreenNextButton" type="button" selector=".fotorama--fullscreen.fotorama-item .fotorama__wrap--toggle-arrows .fotorama__arr--next" timeout="30"/> + <element name="imagePrevButton" type="button" selector=".product.media .fotorama-item .fotorama__wrap--toggle-arrows .fotorama__arr--prev" timeout="30"/> + <element name="imageFullscreenPrevButton" type="button" selector=".fotorama--fullscreen.fotorama-item .fotorama__wrap--toggle-arrows .fotorama__arr--prev" timeout="30"/> + <element name="mainImageForJsActions" type="text" selector="div.fotorama div.fotorama__active img.fotorama__img" timeout="30"/> + <element name="mainImageForJsActionsFullscreen" type="text" selector="div.fotorama div.fotorama__active img.fotorama__img--full" timeout="30"/> + <element name="imageSectionForJsActions" type="text" selector=".fotorama__wrap .fotorama__stage" /> + <element name="imgSelectedInThumbnail" type="block" selector=".fotorama__nav-wrap .fotorama__active .fotorama__loaded img[src*='{{filename}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml index bcdf6ad39124a..029c304873ce2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml @@ -11,7 +11,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <annotations> <features value="Catalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml index acc22f2e611d6..5ea7253619ed9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml @@ -10,18 +10,20 @@ <test name="AdminAddImageToWYSIWYGProductTest"> <annotations> <features value="Catalog"/> - <stories value="MAGETWO-42041-Default WYSIWYG toolbar configuration with Magento Media Gallery"/> - <group value="Catalog"/> + <stories value="Default WYSIWYG toolbar configuration with Magento Media Gallery"/> <title value="Admin should be able to add image to WYSIWYG Editor on Product Page"/> <description value="Admin should be able to add image to WYSIWYG Editor on Product Page"/> <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-84375"/> + <testCaseId value="MC-25763"/> + <group value="catalog"/> </annotations> + <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> + <after> <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> @@ -32,8 +34,9 @@ <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillBasicProductInfo" /> <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab" /> - <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDescription" /> + <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDescription"/> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForDescription" /> + <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDescriptionAgain"/> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse1" /> <waitForLoadingMaskToDisappear stepKey="waitForBrowseModal" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml index 45284e69a54e0..0b29d2edb6615 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml @@ -23,6 +23,7 @@ <requiredEntity createDataKey="createCategory"/> <field key="price">100</field> </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminInBefore"/> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> @@ -31,29 +32,46 @@ <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="loginAsAdmin"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> - <scrollToTopOfPage stepKey="scrollToTopOfPage"/> - <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingButton"/> - <waitForElement selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="waitForCustomerGroupPriceAddButton"/> - <click selector="{{AdminProductFormAdvancedPricingSection.customerGroupPriceAddButton}}" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAndpercent"/> - <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="1" stepKey="fillProductTierPriceQtyInput"/> - <selectOption selector="{{AdminProductFormAdvancedPricingSection.productTierPriceValueTypeSelect('0')}}" userInput="Discount" stepKey="selectProductTierPriceValueType"/> - <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" userInput="0.1" stepKey="selectProductTierPricePriceInput"/> - <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="scrollToTopOfPage"/> + <actionGroup ref="AdminProductFormOpenAdvancedPricingDialogActionGroup" stepKey="clickOnAdvancedPricingButton"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForCustomerGroupPriceAddButton"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="addCustomerGroupAllGroupsQty1PriceDiscountAndpercent"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="fillProductTierPriceQtyInput"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectProductTierPriceValueType"/> + + <actionGroup ref="AdminProductFormAdvancedPricingAddTierPriceActionGroup" stepKey="selectProductTierPricePriceInput"> + <argument name="website" value="{{tierPrice01PercentDiscount.website}}"/> + <argument name="customerGroup" value="{{tierPrice01PercentDiscount.customer_group}}"/> + <argument name="quantity" value="{{tierPrice01PercentDiscount.qty}}"/> + <argument name="priceType" value="Discount"/> + <argument name="amount" value="{{tierPrice01PercentDiscount.price}}"/> + </actionGroup> + + <actionGroup ref="AdminProductFormDoneAdvancedPricingDialogActionGroup" stepKey="clickDoneButton"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct1"/> - <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.sku$$)}}" stepKey="goProductPageOnStorefront"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goProductPageOnStorefront"> + <argument name="productUrl" value="$$createSimpleProduct.sku$$"/> + </actionGroup> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForPageLoad1"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('99.90')}}" stepKey="assertProductFinalPriceProductPage"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceProductPage"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmountProductPage"/> - <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad2"/> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForPageLoad2"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceFinal('99.90')}}" stepKey="assertProductFinalPriceCategoryPage"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabelCategoryPage"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmountCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml index 8d41b276334a6..dd98df9325665 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithOutOfStockChildProductTest.xml @@ -20,6 +20,9 @@ <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity" after="waitForProductPageToLoad"/> <remove keyForRemoval="disableProduct"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock" after="scrollToProductQuantity"/> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="selectOutOfStock" after="scrollToProductQuantity"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml index 9c1ff43587a27..db789d3512acf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -69,10 +69,7 @@ </actionGroup> <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButton"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <!--Run re-index task --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Verify product is visible in category front page --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml index 3332bc66653e5..8d0534891a29b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml @@ -70,10 +70,7 @@ <argument name="targetPath" value="catalog/category/view/id/{$categoryId}"/> </actionGroup> - <!--Clear cache and reindex--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml index 500c95d1120f3..7447c75a778af 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml @@ -30,10 +30,7 @@ <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreViewFr"> <argument name="storeView" value="customStoreFR"/> </actionGroup> - <!--Run full reindex and clear caches --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> @@ -47,9 +44,7 @@ <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="indexerReindex"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -68,10 +63,7 @@ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{CatNotActive.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveCategory"/> - <!--Run full reindex and clear caches --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml index 2394b41502f84..df2124759686d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -30,10 +30,7 @@ <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreViewFr"> <argument name="storeView" value="customStoreFR"/> </actionGroup> - <!--Run full reindex and clear caches --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> @@ -47,9 +44,7 @@ <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="indexerReindex"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -69,9 +64,8 @@ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveIncludeInMenu"/> - <!--Run full reindex and clear caches --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <argument name="indices" value="catalog_category_flat"/> </actionGroup> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml index 35e53273aebf2..2f86209da1eba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -30,10 +30,7 @@ <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreViewFr"> <argument name="storeView" value="customStoreFR"/> </actionGroup> - <!--Run full reindex and clear caches --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> @@ -47,9 +44,7 @@ <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="indexerReindex"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> </actionGroup> @@ -70,9 +65,8 @@ <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="verifyInactiveIncludeInMenu"/> - <!--Run full reindex and clear caches --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <argument name="indices" value="catalog_category_flat"/> </actionGroup> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml index 759087afbf95a..3cf13bbd2f602 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -53,7 +53,9 @@ <waitForPageLoad stepKey="waitForProductToLoad"/> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="selectStockStatus"> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> <!-- Create New Product Attribute --> <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> @@ -88,10 +90,7 @@ <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> - <!--Run Re-Index task --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Verify product attribute added in product form --> <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml index 9a9d64617f7b5..573877f9869d4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml @@ -50,7 +50,9 @@ <waitForPageLoad stepKey="waitForProductToLoad"/> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQty"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="selectStockStatus"/> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="selectStockStatus"> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> <!-- Create Product Attribute --> <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml index 54c3a05651c44..a00714e412b0a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml @@ -31,9 +31,7 @@ <argument name="category" value="$$createPreReqCategory$$"/> <argument name="simpleProduct" value="ProductWithUnicode"/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 1874f0709d9ec..e12394cbcb512 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -117,9 +117,7 @@ <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml index 0c488b86a92c8..e4cacba0224a7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -95,10 +95,7 @@ <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> - <!--Run full reindex and clear caches --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml index 7e5ee977d679b..3514f53e8b937 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml @@ -65,9 +65,7 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{NewWebSiteData.name}}"/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> @@ -96,10 +94,7 @@ <argument name="website" value="{{NewWebSiteData.name}}"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct2"/> - <!--Reindex and flush cache--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml index b437d5fb0c868..283ed72e62faa 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml @@ -20,7 +20,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> <createData stepKey="myProductAttributeCreation" entity="productAttributeWysiwyg"/> <createData stepKey="myProductAttributeSetAssign" entity="AddToDefaultSet"> <requiredEntity createDataKey="myProductAttributeCreation"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberSetsToOneAfterNewSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberSetsToOneAfterNewSearchTest.xml new file mode 100644 index 0000000000000..9bb7064b1870b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberSetsToOneAfterNewSearchTest.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminGridPageNumberSetsToOneAfterNewSearchTest"> + <annotations> + <features value="Catalog"/> + <stories value="Catalog grid"/> + <title value="Updating the search keyword in admin product grid should reset current page to the first one"/> + <description value="When changing the search keyword in admin product grid, new results should be displayed from the page one"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-39332"/> + <useCaseId value="MC-38787"/> + <group value="Catalog"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <comment userInput="Clear product grid" stepKey="commentClearProductGrid"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridToDefaultView"/> + <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProductIfTheyExist"/> + + <!-- Create required prerequisites --> + <createData entity="SimpleSubCategory" stepKey="category1"/> + <createData entity="SimpleProduct" stepKey="simpleProduct1"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct3"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="SimpleProduct" stepKey="simpleProduct4"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="VirtualProduct" stepKey="virtualProduct1"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="VirtualProduct" stepKey="virtualProduct2"> + <requiredEntity createDataKey="category1"/> + </createData> + <createData entity="VirtualProduct" stepKey="virtualProduct3"> + <requiredEntity createDataKey="category1"/> + </createData> + </before> + + <after> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + <actionGroup ref="AdminDataGridDeleteCustomPerPageActionGroup" stepKey="deleteCustomAddedPerPage"> + <argument name="perPage" value="ProductPerPage.productCount"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + + <!-- Delete prerequisites --> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> + <deleteData createDataKey="simpleProduct4" stepKey="deleteSimpleProduct4"/> + <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> + <deleteData createDataKey="virtualProduct2" stepKey="deleteVirtualProduct2"/> + <deleteData createDataKey="virtualProduct3" stepKey="deleteVirtualProduct3"/> + <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> + + <!-- Set the product grid to display one product per page --> + <actionGroup ref="AdminDataGridSelectCustomPerPageActionGroup" stepKey="select1ProductPerPage"> + <argument name="perPage" value="ProductPerPage.productCount"/> + </actionGroup> + + <!-- Performing the first search and assertions --> + <actionGroup ref="AdminSearchGridByStringNoClearActionGroup" stepKey="searchForSimpleProduct"> + <argument name="keyword" value="SimpleProduct"/> + </actionGroup> + <actionGroup ref="AdminGridAssertTotalPageCountActionGroup" stepKey="waitForTotalPagesCountFourToBeVisible"> + <argument name="expectedTotalPageCount" value="4"/> + </actionGroup> + <actionGroup ref="AdminGridGoToNextPageActionGroup" stepKey="clickNextPageProductGrid"/> + <actionGroup ref="AdminGridAssertCurrentPageNumberActionGroup" stepKey="assertCurrentPageIsTwoOnProductGridFirstSearch"> + <argument name="expectedCurrentPageNumber" value="2"/> + </actionGroup> + + <!-- Performing the second search and assertions of successful current page number reset --> + <actionGroup ref="AdminSearchGridByStringNoClearActionGroup" stepKey="searchForVirtualProduct"> + <argument name="keyword" value="VirtualProduct"/> + </actionGroup> + <actionGroup ref="AdminGridAssertTotalPageCountActionGroup" stepKey="waitForTotalPagesCountThreeToBeVisible"> + <argument name="expectedTotalPageCount" value="3"/> + </actionGroup> + <actionGroup ref="AdminGridAssertCurrentPageNumberActionGroup" stepKey="assertCurrentPageIsOneOnProductGridSecondSearch"> + <argument name="expectedCurrentPageNumber" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml index af31b7c1d5c07..4dd76e55f9330 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -31,10 +31,7 @@ <requiredEntity createDataKey="createCategory"/> </createData> - <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml index 479c9e5e5bb19..1871c2acf66e1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml @@ -36,20 +36,27 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> - <!-- Search and select products --> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> - <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox1"/> - <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> - <!-- Mass update attributes --> - <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> - <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> - <waitForPageLoad stepKey="waitForBulkUpdatePage"/> - <seeInCurrentUrl stepKey="seeInUrl" url="catalog/product_action_attribute/edit/"/> - <click selector="{{AdminEditProductAttributesSection.ChangeAttributeNameToggle}}" stepKey="toggleToChangeName"/> - <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> + + <actionGroup ref="AdminCheckProductOnProductGridActionGroup" stepKey="clickCheckbox1"> + <argument name="product" value="$$createProductOne$$"/> + </actionGroup> + + <actionGroup ref="AdminCheckProductOnProductGridActionGroup" stepKey="clickCheckbox2"> + <argument name="product" value="$$createProductTwo$$"/> + </actionGroup> + + <actionGroup ref="AdminClickMassUpdateProductAttributesActionGroup" stepKey="clickDropdown"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickOption"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitForBulkUpdatePage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="seeInUrl"/> + + <actionGroup ref="AdminCheckNameToggleOnProductsMassAttributesUpdateActionGroup" stepKey="toggleToChangeName"/> + + <actionGroup ref="AdminClickSaveOnProductsMassAttributesUpdateActionGroup" stepKey="save"/> <see stepKey="seeError" selector="{{AdminEditProductAttributesSection.NameError}}" userInput="This is a required field"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml new file mode 100644 index 0000000000000..aa82b092a0a98 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductQtyIncrementsTest.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMassUpdateProductQtyIncrementsTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update advanced inventory attributes"/> + <title value="Admin should be able to mass update product qty increments"/> + <description value="Admin should be able to mass update product qty increments"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-39359"/> + <useCaseId value="MC-36787"/> + <group value="catalog"/> + <group value="CatalogInventory"/> + <group value="product_attributes"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProductOne"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProductTwo"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> + <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + </after> + <!-- Navigate to products list page and select created products --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> + <argument name="keyword" value="api-simple-product"/> + </actionGroup> + <actionGroup ref="SortProductsByIdDescendingActionGroup" stepKey="sortProductsByIdDescending"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox1"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckbox2"/> + <!-- Mass update qty increments --> + <actionGroup ref="AdminClickMassUpdateProductAttributesActionGroup" stepKey="clickMassUpdateProductAttributes"/> + <actionGroup ref="AdminMassUpdateProductQtyIncrementsActionGroup" stepKey="updateQtyIncrements"> + <argument name="enableQtyIncrements" value="Yes"/> + <argument name="qtyIncrements" value="2"/> + </actionGroup> + <!-- Start message queue for product attribute consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminProductAttributeUpdateMessageConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminProductAttributeUpdateMessageConsumerData.messageLimit}}"/> + </actionGroup> + <!-- Open first product for edit and assert that qty increment is updated --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createProductOne.id$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <seeOptionIsSelected selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" userInput="Yes" stepKey="assertEnableQtyIncrementsValue"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="assertEnableQtyIncrementsUseConfigSettings"/> + <seeInField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput="2" stepKey="assertQtyIncrementsValue"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="assertQtyIncrementsUseConfigSettings"/> + <!-- Open second product for edit and assert that qty increment is updated --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage2"> + <argument name="productId" value="$createProductTwo.id$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink2"/> + <seeOptionIsSelected selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" userInput="Yes" stepKey="assertEnableQtyIncrementsValue2"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="assertEnableQtyIncrementsUseConfigSettings2"/> + <seeInField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput="2" stepKey="assertQtyIncrementsValue2"/> + <dontSeeCheckboxIsChecked selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="assertQtyIncrementsUseConfigSettings2"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml index 809a015369ea9..3da19eb598012 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -65,10 +65,7 @@ <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> - <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml index 9100e6027a52f..1c55b09151cf3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml @@ -22,10 +22,7 @@ <createData entity="FirstLevelSubCat" stepKey="createDefaultCategory"> <field key="is_active">true</field> </createData> - <!-- Perform reindex and flush cache --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml index 0e056e4bb7078..fe3ffbb4fc1d7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -56,10 +56,7 @@ <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> - <!--Run re-index task --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Verify category displayed in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml index 8e728fc6e1f27..e06a7f3c5679c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml @@ -34,10 +34,7 @@ <argument name="customStore" value="storeViewData"/> </actionGroup> - <!--Run full reindex and clear caches --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml index 1707fda9e3edb..e989aa3758cf3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -65,9 +65,7 @@ </actionGroup> <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml index 73aeed3af4fb0..f5046faf82b6f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml @@ -45,9 +45,7 @@ <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml index 208b588493112..f7f87da77b401 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml @@ -35,9 +35,7 @@ <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> @@ -48,9 +46,7 @@ <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="indexerReindex"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> </actionGroup> @@ -61,10 +57,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!-- Select Created Category--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexBeforeFlow"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexBeforeFlow"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> @@ -82,10 +75,7 @@ <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> - <!--Open Index Management Page and verify flat categoryIndex status--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml index a688dea47a0c4..b316e3194c986 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -64,9 +64,8 @@ <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="enableIncludeInMenuOption"/> <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> - <!--Run full reindex and clear caches --> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <argument name="indices" value="catalog_category_flat"/> </actionGroup> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml index 3b1e9582ebc4c..9e5fd6261ee0e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -42,12 +42,12 @@ </after> <!-- Search default simple product in grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Assign simple product to created store view --> <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="clickCategoryStoreViewDropdownToggle"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml index 62ca93f9bba7b..26380bd2861d4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -42,12 +42,12 @@ </after> <!-- Search default simple product in grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Assign simple product to created store view --> <click selector="{{AdminCategoryMainActionsSection.CategoryStoreViewDropdownToggle}}" stepKey="clickCategoryStoreViewDropdownToggle"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml index d7c5377327193..dbdf4dce301d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml @@ -44,12 +44,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with tier price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="fillSimpleProductName"/> @@ -110,8 +110,11 @@ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductTierPrice300InStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductTierPrice300InStock.weight}}" stepKey="seeSimpleProductWeight"/> <seeInField selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductTierPrice300InStock.weightSelect}}" stepKey="seeSimpleProductWeightSelect"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories"/> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="seeSelectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelectToApplyChanges"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -153,6 +156,6 @@ <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeProductName"> <argument name="productName" value="{{simpleProductTierPrice300InStock.name}}"/> </actionGroup> - + </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml index 38591b836e52a..ab0fcea919af0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml @@ -34,12 +34,12 @@ </after> <!-- Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDisabled.name}}" stepKey="fillSimpleProductName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml new file mode 100644 index 0000000000000..3af6de07e561d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest"> + <annotations> + <stories value="Update Simple Product"/> + <title value="Update Simple Product with Regular Price (In Stock) Enabled Flat"/> + <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Enabled Flat"/> + <testCaseId value="MC-10818"/> + <severity value="CRITICAL"/> + <group value="catalog"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <magentoCLI stepKey="setFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 1"/> + <createData entity="SimpleSubCategory" stepKey="initialCategoryEntity"/> + <createData entity="defaultSimpleProduct" stepKey="initialSimpleProduct"> + <requiredEntity createDataKey="initialCategoryEntity"/> + </createData> + <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> + </before> + <after> + <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> + <deleteData stepKey="deleteSimpleSubCategory2" createDataKey="categoryEntity"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteCreatedProduct"> + <argument name="sku" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI stepKey="unsetFlatCatalogProduct" command="config:set catalog/frontend/flat_catalog_product 0"/> + </after> + + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProductPage"> + <argument name="product" value="$$initialSimpleProduct$$"/> + </actionGroup> + + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickAdvancedInventoryLink"/> + <actionGroup ref="AdminSetManageStockConfigActionGroup" stepKey="setManageStockConfig"> + <argument name="value" value="No"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + + <actionGroup ref="AdminAssignTwoCategoriesToProductActionGroup" stepKey="assignCategories"> + <argument name="categoryName" value="$$initialCategoryEntity.name$$"/> + <argument name="categoryTwoName" value="$$categoryEntity.name$$"/> + </actionGroup> + + <actionGroup ref="AdminFillMainProductFormActionGroup" stepKey="fillSimpleProductInfo"> + <argument name="product" value="simpleProductEnabledFlat"/> + </actionGroup> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> + + <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeSimpleProductSavedSuccessMessage"/> + + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openProductPage1"> + <argument name="product" value="simpleProductEnabledFlat"/> + </actionGroup> + + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickTheAdvancedInventoryLink1"/> + <actionGroup ref="AssertAdminManageStockOnEditPageActionGroup" stepKey="assertManageStock1"> + <argument name="manageStock" value="No"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButtonOnAdvancedInventorySection1"/> + + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="checkifProductIsAssignedToInitialCategory"> + <argument name="categoryName" value="$$initialCategoryEntity.name$$"/> + </actionGroup> + + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="checkifProductIsAssignedToCategoryTwo"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + + <actionGroup ref="AssertAdminProductInfoOnEditPageActionGroup" stepKey="assertProductInfo"> + <argument name="product" value="simpleProductEnabledFlat"/> + </actionGroup> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="category" value="$categoryEntity$"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="seeSimpleProductNameOnCategoryPage"> + <argument name="productName" value="{{simpleProductEnabledFlat.name}}"/> + </actionGroup> + + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage"> + <argument name="productUrlKey" value="{{simpleProductEnabledFlat.urlKey}}"/> + </actionGroup> + + <actionGroup ref="StorefrontAssertProductNameOnProductPageActionGroup" stepKey="seeSimpleProductNameOnStoreFrontPage"> + <argument name="productName" value="{{simpleProductEnabledFlat.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPriceOnProductPageActionGroup" stepKey="seeSimpleProductPriceOnStoreFrontPage"> + <argument name="productPrice" value="{{simpleProductEnabledFlat.price}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSKUOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductStockStatusOnProductPageActionGroup" stepKey="seeSimpleProductStockStatusOnStoreFrontPage"> + <argument name="productStockStatus" value="{{simpleProductEnabledFlat.storefrontStatus}}"/> + </actionGroup> + + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchForSku"> + <argument name="phrase" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductFromQuickSearchActionGroup" stepKey="openAndCheckProduct"> + <argument name="productName" value="{{simpleProductEnabledFlat.name}}"/> + <argument name="productUrlKey" value="{{simpleProductEnabledFlat.urlKey}}"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml index aa1b1ae702914..d2e145adb6a86 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml @@ -8,15 +8,18 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest"> + <test name="AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest" deprecated="Use AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest instead"> <annotations> <stories value="Update Simple Product"/> - <title value="Update Simple Product with Regular Price (In Stock) Enabled Flat"/> + <title value="DEPRECACTED. Update Simple Product with Regular Price (In Stock) Enabled Flat"/> <description value="Test log in to Update Simple Product and Update Simple Product with Regular Price (In Stock) Enabled Flat"/> <testCaseId value="MC-10818"/> <severity value="CRITICAL"/> <group value="catalog"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatCatalogTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -38,12 +41,12 @@ </after> <!-- Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="fillSimpleProductName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml index dd8792e68af2b..f36b76d0f8dcd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,11 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductNotVisibleIndividually.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductNotVisibleIndividually.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductNotVisibleIndividually.weightNoDecimals}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="seeSelectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="seeSelectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductNotVisibleIndividually.visibility}}" stepKey="seeSimpleProductVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml index af3861e4e0b64..7bb5f090edf5d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml @@ -34,12 +34,12 @@ </after> <!--Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product by unselecting categories --> <scrollTo selector="{{AdminProductFormSection.productStockStatus}}" stepKey="scroll"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml index 0158633d77df3..c10cd19eef563 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid page --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,11 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice245InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice245InStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice245InStock.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickDoneOnAdvancedCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice245InStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> @@ -133,6 +136,6 @@ <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeSimpleProductNameOnStorefrontPage"> <argument name="productName" value="{{simpleProductRegularPrice245InStock.name}}"/> </actionGroup> - + </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml index 8946a86fef2ab..ee69bc5ced82b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -36,14 +36,13 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> - </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> + </actionGroup><comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> - <!-- <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> --> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="fillSimpleProductName"/> @@ -88,8 +87,11 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32501InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice32501InStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32501InStock.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickDoneOnAdvancedCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice32501InStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml index 7b6db040ad8e1..9f4b4402fb94d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(in stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,11 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice325InStock.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice325InStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice325InStock.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickDoneOnAdvancedCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice325InStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml index 670030d1d98ea..2316e36eb0b9c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductName"/> @@ -97,8 +97,12 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPriceCustomOptions.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPriceCustomOptions.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPriceCustomOptions.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="seeUrlKey"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml index f440f714e8c39..049d7fc0eaebe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml @@ -36,12 +36,12 @@ </after> <!-- Search default simple product in the grid --> - <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> - <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> - <argument name="sku" value="$$initialSimpleProduct.sku$$"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="openProductCatalogPage"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="filterProductGrid"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="clickFirstRowToOpenDefaultSimpleProduct"> + <argument name="product" value="$$initialSimpleProduct$$"/> </actionGroup> - <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToOpenDefaultSimpleProduct"/> - <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="waitUntilProductIsOpened"/> <!-- Update simple product with regular price(out of stock) --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductName"/> @@ -84,8 +84,11 @@ <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductRegularPrice32503OutOfStock.quantity}}" stepKey="seeSimpleProductQuantity"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductRegularPrice32503OutOfStock.status}}" stepKey="seeSimpleProductStockStatus"/> <seeInField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductRegularPrice32503OutOfStock.weight}}" stepKey="seeSimpleProductWeight"/> - <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDownToVerify"/> - <see selector="{{AdminProductFormSection.selectMultipleCategories}}" userInput="$$categoryEntity.name$$" stepKey="selectedCategories" /> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickCategoriesDropDownToVerify"/> + <actionGroup ref="AssertAdminProductIsAssignedToCategoryActionGroup" stepKey="selectedCategories"> + <argument name="categoryName" value="$$categoryEntity.name$$"/> + </actionGroup> <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickDoneOnAdvancedCategorySelect"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index fbb6893e92b1e..179ac35b9d206 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -10,6 +10,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CLoggedInUserTest"> <before> + <actionGroup ref="DeleteAllProductsUsingProductGridActionGroup" after="loginAsAdmin" stepKey="deleteAllProducts"/> + <createData entity="ApiCategory" stepKey="createCategory"/> <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct1"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml index 507e4ae14e83c..2f38e16a44476 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml @@ -44,7 +44,7 @@ </actionGroup> <actionGroup ref="AdminSetQuantityToEachSkusConfigurableProductActionGroup" stepKey="saveConfigurable"/> <grabValueFrom selector="{{NewProductPageSection.sku}}" stepKey="grabSkuProduct"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <comment userInput="Adding the comment to replace 'indexer:reindex' command for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="expandOption"> <argument name="categoryName" value="$$createCategory.name$$"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml index dc608a7f12dd3..2063054a94d0b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml @@ -23,7 +23,7 @@ <requiredEntity createDataKey="createCategory"/> </createData> <magentoCLI command="config:set {{CatalogInventoryOptionsOnlyXleftThreshold.path}} 10000" stepKey="setStockThresholdQty"/> - <magentoCLI command="cache:flush config" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml index 67ca04a0a4594..1032c322053da 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml @@ -39,10 +39,7 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <!--Run re-index task--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaGalleryBehaviorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaGalleryBehaviorTest.xml new file mode 100644 index 0000000000000..0fd5a7117167c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaGalleryBehaviorTest.xml @@ -0,0 +1,463 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontProductWithMediaGalleryBehaviorTest"> + <annotations> + <features value="Catalog"/> + <stories value="Storefront Gallery behaviour for Product with media"/> + <title value="Assert media behaviour for product with different media on storefront"/> + <description value="Assert media behaviour for product with different media on storefront"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-26305"/> + <group value="catalog"/> + <group value="productVideo"/> + <skip> + <issueId value="MC-33903"/> + </skip> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!--Add images and video to product--> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="openAdminProductEditPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addBaseImage"> + <argument name="image" value="TestImageAdobe"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage1"> + <argument name="image" value="AdobeSmallImage"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage2"> + <argument name="image" value="AdobeThumbImage"/> + </actionGroup> + <actionGroup ref="AdminAddProductVideoWithPreviewActionGroup" stepKey="addVideo"> + <argument name="video" value="VimeoProductVideo"/> + <argument name="image" value="{{TestImageNew.file}}"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage4"> + <argument name="image" value="Magento2"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage5"> + <argument name="image" value="JpgImage"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage6"> + <argument name="image" value="LargeImage"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage7"> + <argument name="image" value="Magento2"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage8"> + <argument name="image" value="MagentoImage"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage9"> + <argument name="image" value="Magento3"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage10"> + <argument name="image" value="TestImageNew"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage11"> + <argument name="image" value="ProductImage"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage12"> + <argument name="image" value="MediumImage"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage13"> + <argument name="image" value="MediumImage"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage14"> + <argument name="image" value="PngImage"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage15"> + <argument name="image" value="Magento2"/> + </actionGroup> + <actionGroup ref="AddProductImageActionGroup" stepKey="addImage16"> + <argument name="image" value="Magento3"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToStorefrontProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <!--Assert positioning images in the ribbon Step 2.3--> + <waitForElementVisible selector="{{StorefrontProductMediaSection.fotoramaAnyMedia}}" stepKey="waitForThumbnailsAppear"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition1"> + <argument name="image" value="{{TestImageAdobe.filename}}"/> + <argument name="extension" value="{{TestImageAdobe.file_extension}}"/> + <argument name="position" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition2"> + <argument name="image" value="{{AdobeSmallImage.filename}}"/> + <argument name="extension" value="{{AdobeSmallImage.file_extension}}"/> + <argument name="position" value="2"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition3"> + <argument name="image" value="{{AdobeThumbImage.filename}}"/> + <argument name="extension" value="{{AdobeThumbImage.file_extension}}"/> + <argument name="position" value="3"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition4"> + <argument name="image" value="{{TestImageNew.filename}}"/> + <argument name="extension" value="{{TestImageNew.file_extension}}"/> + <argument name="position" value="4"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition5"> + <argument name="image" value="{{Magento2.filename}}"/> + <argument name="extension" value="{{Magento2.file_extension}}"/> + <argument name="position" value="5"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition6"> + <argument name="image" value="{{JpgImage.filename}}"/> + <argument name="extension" value="{{JpgImage.file_extension}}"/> + <argument name="position" value="6"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition7"> + <argument name="image" value="{{LargeImage.filename}}"/> + <argument name="extension" value="{{LargeImage.file_extension}}"/> + <argument name="position" value="7"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition8"> + <argument name="image" value="{{Magento2.filename}}"/> + <argument name="extension" value="{{Magento2.file_extension}}"/> + <argument name="position" value="8"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition9"> + <argument name="image" value="{{MagentoImage.filename}}"/> + <argument name="extension" value="{{MagentoImage.file_extension}}"/> + <argument name="position" value="9"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition10"> + <argument name="image" value="{{Magento3.filename}}"/> + <argument name="extension" value="{{Magento3.file_extension}}"/> + <argument name="position" value="10"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition11"> + <argument name="image" value="{{TestImageNew.filename}}"/> + <argument name="extension" value="{{TestImageNew.file_extension}}"/> + <argument name="position" value="11"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition12"> + <argument name="image" value="{{ProductImage.filename}}"/> + <argument name="extension" value="{{ProductImage.file_extension}}"/> + <argument name="position" value="12"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition13"> + <argument name="image" value="{{MediumImage.filename}}"/> + <argument name="extension" value="{{MediumImage.file_extension}}"/> + <argument name="position" value="13"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition14"> + <argument name="image" value="{{MediumImage.filename}}"/> + <argument name="extension" value="{{MediumImage.file_extension}}"/> + <argument name="position" value="14"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition15"> + <argument name="image" value="{{PngImage.filename}}"/> + <argument name="extension" value="{{PngImage.file_extension}}"/> + <argument name="position" value="15"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition16"> + <argument name="image" value="{{Magento2.filename}}"/> + <argument name="extension" value="{{Magento2.file_extension}}"/> + <argument name="position" value="16"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertImagePosition17"> + <argument name="image" value="{{Magento3.filename}}"/> + <argument name="extension" value="{{Magento3.file_extension}}"/> + <argument name="position" value="17"/> + </actionGroup> + <!--Assert fullscreen image isn't displayed. Step 2.1--> + <actionGroup ref="StorefrontAssertActiveProductImageActionGroup" stepKey="seeActiveBaseImage"> + <argument name="fileName" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <dontSeeElement selector="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}" stepKey="dontSeeFullscreenProductImage"/> + <!--Assert thumbnail drag actions. Steps 3-4--> + <actionGroup ref="AssertStorefrontProductPageGalleryThumbnailDragActionGroup" stepKey="assertThumbnailDragAction"> + <argument name="dragPointImage" value="{{TestImageNew.filename}}"/> + <argument name="currentImage" value="{{TestImageAdobe.filename}}"/> + <argument name="firstImage" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <!--Verify if looping is unavailable. Step 5--> + <dontSeeElement selector="{{StorefrontProductMediaSection.fotoramaPrevButton}}" stepKey="dontSeePrevButton"/> + <seeElement selector="{{StorefrontProductMediaSection.fotoramaNextButton}}" stepKey="seeNextButton"/> + <click selector="{{StorefrontProductMediaSection.fotoramaNextButton}}" stepKey="firstClickNextFotorama"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(MagentoImage.filename)}}" stepKey="see9thImageInRibbon"/> + <click selector="{{StorefrontProductMediaSection.fotoramaNextButton}}" stepKey="secondClickNextFotorama"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(MagentoImage.filename)}}" stepKey="seeLastImageInRibbon"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(TestImageAdobe.filename)}}" stepKey="seeActiveImageDontChangeAfterClickNext"/> + <dontSeeElement selector="{{StorefrontProductMediaSection.fotoramaNextButton}}" stepKey="dontSeeNextButtonAfterClickNext"/> + <click selector="{{StorefrontProductMediaSection.fotoramaPrevButton}}" stepKey="firstClickPrevFotorama"/> + <click selector="{{StorefrontProductMediaSection.fotoramaPrevButton}}" stepKey="secondClickPrevFotorama"/> + <seeElement selector="{{StorefrontProductMediaSection.productImage(TestImageAdobe.filename)}}" stepKey="seeActiveImageDefaultStay2"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageInFotorama(TestImageAdobe.filename)}}" stepKey="seeFirstImageInRibbon"/> + <dontSeeElement selector="{{StorefrontProductMediaSection.fotoramaPrevButton}}" stepKey="dontSeePrevButtonAfterClick"/> + <!--Change image by thumbnail ribbon. Step 6--> + <actionGroup ref="AssertStorefrontProductPageGalleryChangingMainImageByRibbonActionGroup" stepKey="assertThumbnailClicking"> + <argument name="startImage" value="{{TestImageAdobe.filename}}"/> + <argument name="expectedImage" value="{{AdobeSmallImage.filename}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductImageAppearsOnProductPagePreviewActionGroup" stepKey="seeImageOnPreview"> + <argument name="productImage" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <!--Change image by image buttons. Step 7--> + <actionGroup ref="AssertStorefrontProductPageGalleryMainImageButtonsActionGroup" stepKey="assertButtonActions"> + <argument name="startImage" value="{{TestImageAdobe.filename}}"/> + <argument name="expectedImage" value="{{AdobeSmallImage.filename}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductImageAppearsOnProductPagePreviewActionGroup" stepKey="seeImageAfterButtonActions"> + <argument name="productImage" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <!--Check that images <= that image section. Step 7.4--> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions0"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert0"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions1"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert1"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions2"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert2"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions3"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert3"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions4"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert4"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions5"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert5"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions6"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert6"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions7"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert7"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions8"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert8"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions9"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert9"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions10"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert10"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions11"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert11"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions12"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert12"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions13"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert13"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions14"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert14"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions15"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert15"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertImageDimensions16"/> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="clickOnNextImageButtonDimensionsAssert16"/> + <actionGroup ref="AssertStorefrontProductImageAppearsOnProductPagePreviewActionGroup" stepKey="seeImageAfterLoop"> + <argument name="productImage" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <!--Change image using the drag actions. Step 8--> + <actionGroup ref="StorefrontProductPageGalleryDragMainImageBackActionGroup" stepKey="dragBack"/> + <actionGroup ref="AssertStorefrontProductImageAppearsOnProductPagePreviewActionGroup" stepKey="seeImageAfterDragBack"> + <argument name="productImage" value="{{Magento3.filename}}"/> + </actionGroup> + <actionGroup ref="StorefrontProductPageGalleryDragMainImageForwardActionGroup" stepKey="dragForward"/> + <moveMouseOver selector="{{StorefrontProductMediaSection.mainImageForJsActions}}" stepKey="hoverOnImageAfterDragActions"/> + <actionGroup ref="AssertStorefrontProductImageAppearsOnProductPagePreviewActionGroup" stepKey="seeImageAfterDragForward"> + <argument name="productImage" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <!--Assert the image looping by image buttons. Step 9--> + <click selector="{{StorefrontProductMediaSection.imagePrevButton}}" stepKey="loopBack"/> + <actionGroup ref="AssertStorefrontProductImageAppearsOnProductPagePreviewActionGroup" stepKey="seeImageAfterLoopBack"> + <argument name="productImage" value="{{Magento3.filename}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="loopForward"/> + <actionGroup ref="AssertStorefrontProductImageAppearsOnProductPagePreviewActionGroup" stepKey="seeImageAfterLoopForward"> + <argument name="productImage" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <!--Open the fullscreen image. Steps 10-11--> + <click selector="{{StorefrontProductMediaSection.imageNextButton}}" stepKey="setNonDefaultImage"/> + <click selector="{{StorefrontProductMediaSection.mainImageForJsActions}}" stepKey="openFullscreenImage"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(AdobeSmallImage.filename)}}" stepKey="assertFullscreenImage"/> + <waitForElementVisible selector="{{StorefrontProductMediaSection.fotoramaAnyMedia}}" stepKey="waitForThumbnailsFullscreen"/> + <!--Assert positioning images in the ribbon Step 11.3--> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition1"> + <argument name="image" value="{{TestImageAdobe.filename}}"/> + <argument name="extension" value="{{TestImageAdobe.file_extension}}"/> + <argument name="position" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition2"> + <argument name="image" value="{{AdobeSmallImage.filename}}"/> + <argument name="extension" value="{{AdobeSmallImage.file_extension}}"/> + <argument name="position" value="2"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition3"> + <argument name="image" value="{{AdobeThumbImage.filename}}"/> + <argument name="extension" value="{{AdobeThumbImage.file_extension}}"/> + <argument name="position" value="3"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition4"> + <argument name="image" value="{{TestImageNew.filename}}"/> + <argument name="extension" value="{{TestImageNew.file_extension}}"/> + <argument name="position" value="4"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition5"> + <argument name="image" value="{{Magento2.filename}}"/> + <argument name="extension" value="{{Magento2.file_extension}}"/> + <argument name="position" value="5"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition6"> + <argument name="image" value="{{JpgImage.filename}}"/> + <argument name="extension" value="{{JpgImage.file_extension}}"/> + <argument name="position" value="6"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition7"> + <argument name="image" value="{{LargeImage.filename}}"/> + <argument name="extension" value="{{LargeImage.file_extension}}"/> + <argument name="position" value="7"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition8"> + <argument name="image" value="{{Magento2.filename}}"/> + <argument name="extension" value="{{Magento2.file_extension}}"/> + <argument name="position" value="8"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition9"> + <argument name="image" value="{{MagentoImage.filename}}"/> + <argument name="extension" value="{{MagentoImage.file_extension}}"/> + <argument name="position" value="9"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition10"> + <argument name="image" value="{{Magento3.filename}}"/> + <argument name="extension" value="{{Magento3.file_extension}}"/> + <argument name="position" value="10"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition11"> + <argument name="image" value="{{TestImageNew.filename}}"/> + <argument name="extension" value="{{TestImageNew.file_extension}}"/> + <argument name="position" value="11"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition12"> + <argument name="image" value="{{ProductImage.filename}}"/> + <argument name="extension" value="{{ProductImage.file_extension}}"/> + <argument name="position" value="12"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition13"> + <argument name="image" value="{{MediumImage.filename}}"/> + <argument name="extension" value="{{MediumImage.file_extension}}"/> + <argument name="position" value="13"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition14"> + <argument name="image" value="{{MediumImage.filename}}"/> + <argument name="extension" value="{{MediumImage.file_extension}}"/> + <argument name="position" value="14"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition15"> + <argument name="image" value="{{PngImage.filename}}"/> + <argument name="extension" value="{{PngImage.file_extension}}"/> + <argument name="position" value="15"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition16"> + <argument name="image" value="{{Magento2.filename}}"/> + <argument name="extension" value="{{Magento2.file_extension}}"/> + <argument name="position" value="16"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductPageGalleryImagePositionInThumbnailRibbonActionGroup" stepKey="assertFullscreenThumbnailImagePosition17"> + <argument name="image" value="{{Magento3.filename}}"/> + <argument name="extension" value="{{Magento3.file_extension}}"/> + <argument name="position" value="17"/> + </actionGroup> + <!--Assert the fullscreen thumbnail ribbon drag actions step 12--> + <actionGroup ref="AssertStorefrontProductPageGalleryFullscreenThumbnailDragActionGroup" stepKey="assertFullscreenThumbnailDragAction"> + <argument name="dragPointImage" value="{{TestImageNew.filename}}"/> + <argument name="currentImage" value="{{AdobeSmallImage.filename}}"/> + <argument name="firstImage" value="{{TestImageAdobe.filename}}"/> + </actionGroup> + <!--Change fullscreen image by clicking on thumbnail ribbon. Step 15--> + <actionGroup ref="AssertStorefrontProductPageGalleryChangingFullscreenImageByRibbonActionGroup" stepKey="assertThumbnailClickFullscreen"> + <argument name="startImage" value="{{AdobeSmallImage.filename}}"/> + <argument name="expectedImage" value="{{LargeImage.filename}}"/> + </actionGroup> + <!--Change fullscreen image using the image buttons. Steps 16 and 18--> + <click selector="{{StorefrontProductMediaSection.imageFullscreenPrevButton}}" stepKey="goToFirstImage"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(TestImageAdobe.filename)}}" stepKey="seeFirstFullscreenImage"/> + <click selector="{{StorefrontProductMediaSection.imageFullscreenPrevButton}}" stepKey="loopToLastImage"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(Magento3.filename)}}" stepKey="assertLastImageAfterLoop"/> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="loopToFirstImage"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(TestImageAdobe.filename)}}" stepKey="assertFirstImageAfterLoop"/> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert0"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions0"> + <argument name="source" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <!-- Check that images <= that image section. Step 16.5--> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert1"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions1"> + <argument name="source" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert2"/> + <click selector="{{StorefrontProductMediaSection.fotoramaNextButtonVideo}}" stepKey="skipVideo"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions3"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert4"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions4"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert5"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions5"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert6"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions6"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert7"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions7"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert8"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions8"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert9"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions9"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert10"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions10"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert11"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions11"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert12"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions12"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert13"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions13"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert14"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions14"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert15"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions15"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <click selector="{{StorefrontProductMediaSection.imageFullscreenNextButton}}" stepKey="clickOnNextImageButtonFullscreenDimensionsAssert16"/> + <actionGroup ref="AssertStorefrontProductPageGalleryImageDimensionsActionGroup" stepKey="assertFullscreenImageDimensions16"> + <argument name="imageSource" value="{{StorefrontProductMediaSection.mainImageForJsActionsFullscreen}}"/> + </actionGroup> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(TestImageAdobe.filename)}}" stepKey="assertFirstImageAfterSecondLoop"/> + <!-- TODO: Change fullscreen image by drag/swipe action: after MQE-2333 implementation --> + <!--Steps 19-20--> + <click selector="{{StorefrontProductMediaSection.imageFullscreenPrevButton}}" stepKey="selectLastImage"/> + <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(Magento3.filename)}}" stepKey="assertLastImageFullscreen"/> + <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage"/> + <actionGroup ref="AssertStorefrontProductImageAppearsOnProductPagePreviewActionGroup" stepKey="seeLastImageAfterFullscreen"> + <argument name="productImage" value="{{Magento3.filename}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml index 2080aee933aad..814356bb05a22 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -48,6 +48,8 @@ </before> <after> + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> @@ -70,7 +72,7 @@ <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <comment userInput="BIC workaround" stepKey="customerLogoutStorefront"/> </after> <!-- Open Product Grid, Filter product and open --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index 631d1d50077e9..71041ee7e1349 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -31,14 +31,15 @@ <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> </before> <after> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Delete product and category --> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderListingFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> - <!-- Logout customer --> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <comment userInput="BIC workaround" stepKey="customerLogoutStorefront"/> </after> <!-- Login Customer Storefront --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml index aac76999636b0..ce419167e9514 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml @@ -33,6 +33,8 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifyDefaultWYSIWYGToolbarOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifyDefaultWYSIWYGToolbarOnProductTest.xml index 456abecc63ccb..662a251be3b20 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifyDefaultWYSIWYGToolbarOnProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifyDefaultWYSIWYGToolbarOnProductTest.xml @@ -20,7 +20,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4"/> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4"/> </before> <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToProduct"/> <waitForPageLoad stepKey="wait"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifydefaultcontrolsonproductshortdescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifydefaultcontrolsonproductshortdescriptionTest.xml index e929cbd752f81..d4bc095dbe9b1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifydefaultcontrolsonproductshortdescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyDefaultWYSIWYGToolbarOnProductTest/VerifydefaultcontrolsonproductshortdescriptionTest.xml @@ -20,7 +20,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4"/> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4"/> </before> <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToProduct"/> <waitForPageLoad stepKey="wait"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml index d7e4f97ed0bc2..ee6ff0c224545 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml @@ -21,7 +21,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToNewCatalog"/> <waitForLoadingMaskToDisappear stepKey="wait2" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml index cffc4af6fcbbd..f75053f495c4d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml @@ -20,7 +20,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <amOnPage url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'simple')}}" stepKey="navigateToNewProduct"/> <waitForPageLoad stepKey="wait1"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php index 09ad8bb41de7c..c14bb7f524d03 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Type/PriceTest.php @@ -263,4 +263,40 @@ function () { ); } } + + /** + * Get tier price with percent value type + * + * @return void + */ + public function testGetPricesWithPercentType(): void + { + $tierPrices = [ + 0 => [ + 'record_id' => 0, + 'cust_group' => 3200, + 'price_qty' => 3, + 'website_id' => 0, + 'value_type' => 'percent', + 'percentage_value' => 10, + ], + ]; + $this->product->setData('tier_price', $tierPrices); + $this->tpFactory->expects($this->any()) + ->method('create') + ->willReturnCallback( + function () { + return $this->objectManagerHelper->getObject(TierPrice::class); + } + ); + $tierPriceExtensionMock = $this->getMockBuilder(ProductTierPriceExtensionInterface::class) + ->onlyMethods(['getPercentageValue', 'setPercentageValue']) + ->getMockForAbstractClass(); + $tierPriceExtensionMock->method('getPercentageValue') + ->willReturn(50); + $this->tierPriceExtensionFactoryMock->method('create') + ->willReturn($tierPriceExtensionMock); + + $this->assertInstanceOf(TierPrice::class, $this->model->getTierPrices($this->product)[0]); + } } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php index f4334bc25efd8..298595b3d0f62 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php @@ -25,4 +25,58 @@ protected function _productLimitationJoinPrice() $this->_productLimitationFilters->setUsePriceIndex(false); return $this->_productLimitationPrice(true); } + + /** + * Return approximately amount if too much entities. + * + * @return int|mixed + */ + public function getSize() + { + $sql = $this->getSelectCountSql(); + $possibleCount = $this->analyzeCount($sql); + + if ($possibleCount > 20000) { + return $possibleCount; + } + + return parent::getSize(); + } + + /** + * Analyze amount of entities in DB. + * + * @param $sql + * @return int|mixed + * @throws \Zend_Db_Statement_Exception + */ + private function analyzeCount($sql) + { + $results = $this->getConnection()->query('EXPLAIN ' . $sql)->fetchAll(); + $alias = $this->getMainTableAlias(); + + foreach ($results as $result) { + if ($result['table'] == $alias) { + return $result['rows']; + } + } + + return 0; + } + + /** + * Identify main table alias or its name if alias is not defined. + * + * @return string + * @throws \LogicException + */ + private function getMainTableAlias() + { + foreach ($this->getSelect()->getPart(\Magento\Framework\DB\Select::FROM) as $tableAlias => $tableMetadata) { + if ($tableMetadata['joinType'] == 'from') { + return $tableAlias; + } + } + throw new \LogicException("Main table cannot be identified."); + } } diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml index ddd66a5bf04bd..ce34914a2f5d4 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -547,6 +547,8 @@ default="0" comment="Product ID"/> <column xsi:type="smallint" name="store_id" unsigned="true" nullable="true" identity="false" comment="Store ID"/> + <column xsi:type="int" name="list_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="List ID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="catalog_compare_item_id"/> </constraint> @@ -558,6 +560,8 @@ referenceColumn="entity_id" onDelete="CASCADE"/> <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_ITEM_STORE_ID_STORE_STORE_ID" table="catalog_compare_item" column="store_id" referenceTable="store" referenceColumn="store_id" onDelete="SET NULL"/> + <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_ITEM_LIST_ID_CATALOG_COMPARE_LIST_LIST_ID" table="catalog_compare_item" + column="list_id" referenceTable="catalog_compare_list" referenceColumn="list_id" onDelete="CASCADE"/> <index referenceId="CATALOG_COMPARE_ITEM_PRODUCT_ID" indexType="btree"> <column name="product_id"/> </index> @@ -573,6 +577,25 @@ <column name="store_id"/> </index> </table> + <table name="catalog_compare_list" resource="default" engine="innodb" comment="Catalog Compare List with hash Table"> + <column xsi:type="int" name="list_id" padding="10" unsigned="true" nullable="false" + identity="true" comment="Compare List ID"/> + <column xsi:type="varchar" name="list_id_mask" nullable="true" length="32" comment="Masked ID"/> + <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="true" identity="false" + comment="Customer ID"/> + <constraint xsi:type="primary" referenceId="PRIMARY"> + <column name="list_id"/> + </constraint> + <constraint xsi:type="unique" referenceId="CATALOG_COMPARE_LIST_CUSTOMER_ID"> + <column name="customer_id"/> + </constraint> + <constraint xsi:type="foreign" referenceId="CATALOG_COMPARE_LIST_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + table="catalog_compare_list" column="customer_id" referenceTable="customer_entity" + referenceColumn="entity_id" onDelete="CASCADE"/> + <index referenceId="CATALOG_COMPARE_LIST_LIST_ID_MASK" indexType="btree"> + <column name="list_id_mask"/> + </index> + </table> <table name="catalog_product_website" resource="default" engine="innodb" comment="Catalog Product To Website Linkage Table"> <column xsi:type="int" name="product_id" unsigned="true" nullable="false" identity="false" diff --git a/app/code/Magento/Catalog/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json index f4cda73c371d0..efc45112920e2 100644 --- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json +++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json @@ -308,7 +308,8 @@ "visitor_id": true, "customer_id": true, "product_id": true, - "store_id": true + "store_id": true, + "list_id": true }, "index": { "CATALOG_COMPARE_ITEM_PRODUCT_ID": true, @@ -320,7 +321,8 @@ "PRIMARY": true, "CATALOG_COMPARE_ITEM_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID": true, "CATALOG_COMPARE_ITEM_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID": true, - "CATALOG_COMPARE_ITEM_STORE_ID_STORE_STORE_ID": true + "CATALOG_COMPARE_ITEM_STORE_ID_STORE_STORE_ID": true, + "CATALOG_COMPARE_ITEM_LIST_ID_CATALOG_COMPARE_LIST_LIST_ID": true } }, "catalog_product_website": { @@ -1122,5 +1124,20 @@ "CATALOG_PRODUCT_FRONTEND_ACTION_VISITOR_ID_PRODUCT_ID_TYPE_ID": true, "CATALOG_PRODUCT_FRONTEND_ACTION_CUSTOMER_ID_PRODUCT_ID_TYPE_ID": true } + }, + "catalog_compare_list": { + "column": { + "list_id": true, + "list_id_mask": true, + "customer_id": true + }, + "index": { + "CATALOG_COMPARE_LIST_LIST_ID_MASK": true + }, + "constraint": { + "PRIMARY": true, + "CATALOG_COMPARE_LIST_CUSTOMER_ID": true, + "CATALOG_COMPARE_LIST_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID": true + } } } diff --git a/app/code/Magento/Catalog/etc/mview.xml b/app/code/Magento/Catalog/etc/mview.xml index 7ae38a7f2d0e1..2c9d7d448afd2 100644 --- a/app/code/Magento/Catalog/etc/mview.xml +++ b/app/code/Magento/Catalog/etc/mview.xml @@ -49,6 +49,7 @@ <table name="catalog_product_entity_decimal" entity_column="entity_id" /> <table name="catalog_product_entity_int" entity_column="entity_id" /> <table name="catalog_product_entity_tier_price" entity_column="entity_id" /> + <table name="catalog_product_link" entity_column="product_id" /> </subscriptions> </view> <view id="catalog_product_attribute" class="Magento\Catalog\Model\Indexer\Product\Eav" group="indexer"> @@ -56,6 +57,7 @@ <table name="catalog_product_entity_decimal" entity_column="entity_id" /> <table name="catalog_product_entity_int" entity_column="entity_id" /> <table name="catalog_product_entity_varchar" entity_column="entity_id" /> + <table name="catalog_product_link" entity_column="product_id" /> </subscriptions> </view> </config> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml index 5ca88689b9e5f..d786f843e052f 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml @@ -10,11 +10,6 @@ $blockId = $block->getId(); <div id="product_composite_configure" class="product-configure-popup product-configure-popup-<?= $block->escapeHtmlAttr($blockId) ?>"> <iframe name="product_composite_configure_iframe" id="product_composite_configure_iframe"></iframe> - <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( - 'onload', - "window.productConfigure && productConfigure.onLoadIFrame()", - 'iframe[name=\'product_composite_configure_iframe\']:last-of-type' - ) ?> <form action="" method="post" id="product_composite_configure_form" enctype="multipart/form-data" target="product_composite_configure_iframe" class="product_composite_configure_form"> @@ -85,3 +80,8 @@ script; ?> <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> </div> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onload', + "window.productConfigure && productConfigure.onLoadIFrame()", + 'iframe[name=\'product_composite_configure_iframe\']:last-of-type' +) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml index 344123cbe5640..646512d98582e 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml @@ -334,7 +334,7 @@ if (!is_numeric($defaultMinSaleQty)) { </div> <div class="field choice"> <input type="checkbox" id="inventory_use_config_enable_qty_increments" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_enable_qty_increments]" + name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[use_config_enable_qty_inc]" value="1" data-role="toggle-editability" checked="checked" diff --git a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js index 1ac2a4ffadaae..4040ff9d684f4 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/catalog/product/composite/configure.js @@ -607,6 +607,7 @@ define([ * @param method can be 'item_confirm', 'item_restore', 'current_confirmed_to_form', 'form_confirmed_to_confirmed' */ _processFieldsData: function (method) { + var self = this; /** * Internal function for rename fields names of some list type @@ -616,12 +617,14 @@ define([ * @param blockItem */ var _renameFields = function (method, blockItem, listType) { - var pattern = null; - var patternFlat = null; - var replacement = null; - var replacementFlat = null; - var scopeArr = blockItem.id.match(/.*\[\w+\]\[([^\]]+)\]$/); - var itemId = scopeArr[1]; + var pattern = null; + var patternFlat = null; + var patternPrefix = RegExp('\\s', 'g'); + var replacement = null; + var replacementFlat = null; + var replacementPrefix = '_'; + var scopeArr = blockItem.id.match(/.*\[\w+\]\[([^\]]+)\]$/); + var itemId = scopeArr[1]; if (method == 'current_confirmed_to_form') { pattern = RegExp('(\\w+)(\\[?)'); @@ -651,6 +654,14 @@ define([ var rename = function (elms) { for (var i = 0; i < elms.length; i++) { if (elms[i].name && elms[i].type == 'file') { + var prefixName = 'options[files_prefix]', + prefixValue = 'item_' + itemId + '_'; + + self.blockFormFields.insert(new Element('input', { + type: 'hidden', + name: prefixName.replace(pattern, replacement), + value: prefixValue.replace(patternPrefix, replacementPrefix) + })); elms[i].name = elms[i].name.replace(patternFlat, replacementFlat); } else if (elms[i].name) { elms[i].name = elms[i].name.replace(pattern, replacement); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php index b0f085932bb8e..201c70913ca39 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/AttributeQuery.php @@ -148,7 +148,7 @@ private function getAttributesFromEntityTable( ): Select { $select = $connection->select() ->from(['e' => $entityTableName], $entityTableAttributes) - ->where('e.entity_id IN (?)', $entityIds); + ->where('e.entity_id IN (?)', $entityIds, \Zend_Db::INT_TYPE); return $select; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryUidsArgsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryUidsArgsProcessor.php new file mode 100644 index 0000000000000..e091be32698c7 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryUidsArgsProcessor.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Category; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; + +/** + * Category UID processor class for category uid and category id arguments + */ +class CategoryUidsArgsProcessor implements ArgumentsProcessorInterface +{ + private const ID = 'ids'; + + private const UID = 'category_uid'; + + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + + /** + * Composite processor that loops through available processors for arguments that come from graphql input + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $filterKey = 'filters'; + $idFilter = $args[$filterKey][self::ID] ?? []; + $uidFilter = $args[$filterKey][self::UID] ?? []; + if (!empty($idFilter) + && !empty($uidFilter) + && ($fieldName === 'categories' || $fieldName === 'categoryList')) { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID]) + ); + } elseif (!empty($uidFilter)) { + if (isset($uidFilter['eq'])) { + $args[$filterKey][self::ID]['eq'] = $this->uidEncoder->decode( + $uidFilter['eq'] + ); + } elseif (!empty($uidFilter['in'])) { + foreach ($uidFilter['in'] as $uids) { + $args[$filterKey][self::ID]['in'][] = $this->uidEncoder->decode($uids); + } + } + unset($args[$filterKey][self::UID]); + } + return $args; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php b/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php index d2c1fc8f7be9f..675118b953102 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/Hydrator.php @@ -10,6 +10,8 @@ use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\Reflection\DataObjectProcessor; /** @@ -27,16 +29,23 @@ class Hydrator */ private $dataObjectProcessor; + /** @var Uid */ + private $uidEncoder; + /** * @param CustomAttributesFlattener $flattener * @param DataObjectProcessor $dataObjectProcessor + * @param Uid|null $uidEncoder */ public function __construct( CustomAttributesFlattener $flattener, - DataObjectProcessor $dataObjectProcessor + DataObjectProcessor $dataObjectProcessor, + Uid $uidEncoder = null ) { $this->flattener = $flattener; $this->dataObjectProcessor = $dataObjectProcessor; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -54,6 +63,7 @@ public function hydrateCategory(Category $category, $basicFieldsOnly = false) : $categoryData = $this->dataObjectProcessor->buildOutputDataArray($category, CategoryInterface::class); } $categoryData['id'] = $category->getId(); + $categoryData['uid'] = $this->uidEncoder->encode((string) $category->getId()); $categoryData['children'] = []; $categoryData['available_sort_by'] = $category->getAvailableSortBy(); $categoryData['model'] = $category; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php index 4d7ce13fd23cc..0e653995ebcab 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php @@ -7,14 +7,15 @@ namespace Magento\CatalogGraphQl\Model\Resolver; +use Magento\CatalogGraphQl\Model\Category\CategoryFilter; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; -use Magento\CatalogGraphQl\Model\Category\CategoryFilter; /** * Categories resolver, used for GraphQL category data request processing. @@ -36,19 +37,27 @@ class CategoriesQuery implements ResolverInterface */ private $extractDataFromCategoryTree; + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @param CategoryTree $categoryTree * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree * @param CategoryFilter $categoryFilter + * @param ArgumentsProcessorInterface $argsSelection */ public function __construct( CategoryTree $categoryTree, ExtractDataFromCategoryTree $extractDataFromCategoryTree, - CategoryFilter $categoryFilter + CategoryFilter $categoryFilter, + ArgumentsProcessorInterface $argsSelection ) { $this->categoryTree = $categoryTree; $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; $this->categoryFilter = $categoryFilter; + $this->argsSelection = $argsSelection; } /** @@ -70,7 +79,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } try { - $filterResult = $this->categoryFilter->getResult($args, $store, [], $context); + $processedArgs = $this->argsSelection->process($info->fieldName, $args); + $filterResult = $this->categoryFilter->getResult($processedArgs, $store, [], $context); } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php index dcd6f816088dd..04c1754e69eb8 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php @@ -9,6 +9,8 @@ use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; /** * Breadcrumbs data provider @@ -20,13 +22,20 @@ class Breadcrumbs */ private $collectionFactory; + /** @var Uid */ + private $uidEncoder; + /** * @param CollectionFactory $collectionFactory + * @param Uid|null $uidEncoder */ public function __construct( - CollectionFactory $collectionFactory + CollectionFactory $collectionFactory, + Uid $uidEncoder = null ) { $this->collectionFactory = $collectionFactory; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -52,6 +61,7 @@ public function getData(string $categoryPath): array foreach ($collection as $category) { $breadcrumbsData[] = [ 'category_id' => $category->getId(), + 'category_uid' => $this->uidEncoder->encode((string) $category->getId()), 'category_name' => $category->getName(), 'category_level' => $category->getLevel(), 'category_url_key' => $category->getUrlKey(), diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php index 13db03bb2766b..747e05806a821 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -7,14 +7,15 @@ namespace Magento\CatalogGraphQl\Model\Resolver; +use Magento\CatalogGraphQl\Model\Category\CategoryFilter; +use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree; use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree; -use Magento\CatalogGraphQl\Model\Category\CategoryFilter; /** * Category List resolver, used for GraphQL category data request processing. @@ -36,19 +37,27 @@ class CategoryList implements ResolverInterface */ private $extractDataFromCategoryTree; + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @param CategoryTree $categoryTree * @param ExtractDataFromCategoryTree $extractDataFromCategoryTree * @param CategoryFilter $categoryFilter + * @param ArgumentsProcessorInterface $argsSelection */ public function __construct( CategoryTree $categoryTree, ExtractDataFromCategoryTree $extractDataFromCategoryTree, - CategoryFilter $categoryFilter + CategoryFilter $categoryFilter, + ArgumentsProcessorInterface $argsSelection ) { $this->categoryTree = $categoryTree; $this->extractDataFromCategoryTree = $extractDataFromCategoryTree; $this->categoryFilter = $categoryFilter; + $this->argsSelection = $argsSelection; } /** @@ -65,7 +74,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $args['filters']['ids'] = ['eq' => $store->getRootCategoryId()]; } try { - $filterResults = $this->categoryFilter->getResult($args, $store, [], $context); + $processedArgs = $this->argsSelection->process($info->fieldName, $args); + $filterResults = $this->categoryFilter->getResult($processedArgs, $store, [], $context); + $rootCategoryIds = $filterResults['category_ids']; } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php index 701ee70204486..2cfb78418bbae 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToId.php @@ -16,9 +16,10 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; /** - * @inheritdoc - * * Fixed the id related data in the product data + * + * @deprecated Use UID + * @see \Magento\CatalogGraphQl\Model\Resolver\Product\EntityIdToUid */ class EntityIdToId implements ResolverInterface { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToUid.php new file mode 100644 index 0000000000000..90d36e3ed8c82 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/EntityIdToUid.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * The uid related data in the product graphql interface type + */ +class EntityIdToUid implements ResolverInterface +{ + /** + * @var MetadataPool + */ + private $metadataPool; + + /** @var Uid */ + private $uidEncoder; + + /** + * @param MetadataPool $metadataPool + * @param Uid $uidEncoder + */ + public function __construct( + MetadataPool $metadataPool, + Uid $uidEncoder + ) { + $this->metadataPool = $metadataPool; + $this->uidEncoder = $uidEncoder; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var Product $product */ + $product = $value['model']; + + $productId = $product->getData( + $this->metadataPool->getMetadata(ProductInterface::class)->getIdentifierField() + ); + + return $this->uidEncoder->encode((string) $productId); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php index 7aec66ccb699f..aff8fa8a6fc6d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Identity.php @@ -8,6 +8,8 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; /** * Identity for resolved products @@ -15,7 +17,8 @@ class Identity implements IdentityInterface { /** @var string */ - private $cacheTag = \Magento\Catalog\Model\Product::CACHE_TAG; + private $cacheTagProduct = Product::CACHE_TAG; + private $cacheTagCategory = Category::CACHE_TAG; /** * Get product ids for cache tag @@ -26,12 +29,19 @@ class Identity implements IdentityInterface public function getIdentities(array $resolvedData): array { $ids = []; + $categories = $resolvedData['categories'] ?? []; $items = $resolvedData['items'] ?? []; + foreach ($categories as $category) { + $ids[] = sprintf('%s_%s', $this->cacheTagCategory, $category); + } + if (!empty($categories)) { + array_unshift($ids, $this->cacheTagCategory); + } foreach ($items as $item) { - $ids[] = sprintf('%s_%s', $this->cacheTag, $item['entity_id']); + $ids[] = sprintf('%s_%s', $this->cacheTagProduct, $item['entity_id']); } if (!empty($ids)) { - array_unshift($ids, $this->cacheTag); + array_unshift($ids, $this->cacheTagProduct); } return $ids; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php index 8843ad02320c6..3bcc69f94cda0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php @@ -9,6 +9,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Framework\GraphQl\Config\Element\Field; @@ -22,6 +23,17 @@ */ class MediaGalleryEntries implements ResolverInterface { + /** @var Uid */ + private $uidEncoder; + + /** + * Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + /** * @inheritdoc * @@ -53,6 +65,7 @@ public function resolve( if (!empty($product->getMediaGalleryEntries())) { foreach ($product->getMediaGalleryEntries() as $key => $entry) { $mediaGalleryEntries[$key] = $entry->getData(); + $mediaGalleryEntries[$key]['uid'] = $this->uidEncoder->encode((string) $entry->getId()); if ($entry->getExtensionAttributes() && $entry->getExtensionAttributes()->getVideoContent()) { $mediaGalleryEntries[$key]['video_content'] = $entry->getExtensionAttributes()->getVideoContent()->getData(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php index 76602288039c5..f735ab846689f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Options.php @@ -7,8 +7,10 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; @@ -20,6 +22,23 @@ */ class Options implements ResolverInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + + /** @var Uid */ + private $uidEncoder; + + /** + * Uid|null $uidEncoder + */ + public function __construct(Uid $uidEncoder = null) + { + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); + } + /** * @inheritdoc * @@ -55,7 +74,9 @@ public function resolve( $options[$key] = $option->getData(); $options[$key]['required'] = $option->getIsRequire(); $options[$key]['product_sku'] = $option->getProductSku(); - + $options[$key]['uid'] = $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $option->getOptionId() + ); $values = $option->getValues() ?: []; /** @var Option\Value $value */ foreach ($values as $valueKey => $value) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php index 805571d58d634..ed5ae433dd5b7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php @@ -63,6 +63,13 @@ public function resolve( $product = $value['model']; $product->unsetData('minimal_price'); + if ($context) { + $customerGroupId = $context->getExtensionAttributes()->getCustomerGroupId(); + if ($customerGroupId !== null) { + $product->setCustomerGroupId($customerGroupId); + } + } + $requestedFields = $info->getFieldSelection(10); $returnArray = []; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index ba158fab0120c..eebcbfba55b1f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -79,6 +79,11 @@ public function resolve( 'layer_type' => isset($args['search']) ? Resolver::CATALOG_LAYER_SEARCH : Resolver::CATALOG_LAYER_CATEGORY, ]; + if (isset($args['filter']['category_id'])) { + $data['categories'] = $args['filter']['category_id']['eq'] ?? $args['filter']['category_id']['in']; + $data['categories'] = is_array($data['categories']) ? $data['categories'] : [$data['categories']]; + } + return $data; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php index 4a124d69bd20f..03e8358b1ee7a 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch/ProductCollectionSearchCriteriaBuilder.php @@ -61,7 +61,7 @@ public function build(SearchCriteriaInterface $searchCriteria): SearchCriteriaIn foreach ($filterGroup->getFilters() as $filter) { if ($filter->getField() == CategoryProductLink::KEY_CATEGORY_ID) { $categoryFilter = $this->filterBuilder - ->setField($filter->getField()) + ->setField(CategoryProductLink::KEY_CATEGORY_ID) ->setValue($filter->getValue()) ->setConditionType($filter->getConditionType()) ->create(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUidArgsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUidArgsProcessor.php new file mode 100644 index 0000000000000..33845c6dcce6e --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUidArgsProcessor.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; + +/** + * Category UID processor class for category uid and category id arguments + */ +class CategoryUidArgsProcessor implements ArgumentsProcessorInterface +{ + private const ID = 'category_id'; + + private const UID = 'category_uid'; + + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + + /** + * Composite processor that loops through available processors for arguments that come from graphql input + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $idFilter = $args['filter'][self::ID] ?? []; + $uidFilter = $args['filter'][self::UID] ?? []; + if (!empty($idFilter) + && !empty($uidFilter) + && $fieldName === 'products') { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID]) + ); + } elseif (!empty($uidFilter)) { + if (isset($uidFilter['eq'])) { + $args['filter'][self::ID]['eq'] = $this->uidEncoder->decode((string) $uidFilter['eq']); + } elseif (!empty($uidFilter['in'])) { + foreach ($uidFilter['in'] as $uid) { + $args['filter'][self::ID]['in'][] = $this->uidEncoder->decode((string) $uid); + } + } + unset($args['filter'][self::UID]); + } + return $args; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php index d70a3aa7e63c3..0f482fd12e4e3 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\Product; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\InputException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder as SearchCriteriaBuilder; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductProvider; @@ -19,6 +20,8 @@ use Magento\Search\Model\Query; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; /** * Retrieve filtered product data based off given search criteria in a format that GraphQL can interpret. @@ -50,25 +53,34 @@ class Filter implements ProductQueryInterface */ private $scopeConfig; + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @param SearchResultFactory $searchResultFactory * @param ProductProvider $productDataProvider * @param FieldSelection $fieldSelection * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param ScopeConfigInterface $scopeConfig + * @param ArgumentsProcessorInterface|null $argsSelection */ public function __construct( SearchResultFactory $searchResultFactory, ProductProvider $productDataProvider, FieldSelection $fieldSelection, SearchCriteriaBuilder $searchCriteriaBuilder, - ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + ArgumentsProcessorInterface $argsSelection = null ) { $this->searchResultFactory = $searchResultFactory; $this->productDataProvider = $productDataProvider; $this->fieldSelection = $fieldSelection; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->scopeConfig = $scopeConfig; + $this->argsSelection = $argsSelection ? : ObjectManager::getInstance() + ->get(ArgumentsProcessorInterface::class); } /** @@ -78,6 +90,7 @@ public function __construct( * @param ResolveInfo $info * @param ContextInterface $context * @return SearchResult + * @throws GraphQlInputException */ public function getResult( array $args, @@ -86,10 +99,10 @@ public function getResult( ): SearchResult { $fields = $this->fieldSelection->getProductsFieldSelection($info); try { - $searchCriteria = $this->buildSearchCriteria($args, $info); + $searchCriteria = $this->buildSearchCriteria($info->fieldName, $args); $searchResults = $this->productDataProvider->getList($searchCriteria, $fields, false, false, $context); } catch (InputException $e) { - return $this->createEmptyResult($args); + return $this->createEmptyResult((int)$args['pageSize'], (int)$args['currentPage']); } $productArray = []; @@ -120,19 +133,22 @@ public function getResult( /** * Build search criteria from query input args * + * @param string $fieldName * @param array $args - * @param ResolveInfo $info * @return SearchCriteriaInterface + * @throws GraphQlInputException + * @throws InputException */ - private function buildSearchCriteria(array $args, ResolveInfo $info): SearchCriteriaInterface + private function buildSearchCriteria(string $fieldName, array $args): SearchCriteriaInterface { - if (!empty($args['filter'])) { - $args['filter'] = $this->formatFilters($args['filter']); + $processedArgs = $this->argsSelection->process($fieldName, $args); + if (!empty($processedArgs['filter'])) { + $processedArgs['filter'] = $this->formatFilters($processedArgs['filter']); } - $criteria = $this->searchCriteriaBuilder->build($info->fieldName, $args); - $criteria->setCurrentPage($args['currentPage']); - $criteria->setPageSize($args['pageSize']); + $criteria = $this->searchCriteriaBuilder->build($fieldName, $processedArgs); + $criteria->setCurrentPage($processedArgs['currentPage']); + $criteria->setPageSize($processedArgs['pageSize']); return $criteria; } @@ -175,17 +191,18 @@ private function formatFilters(array $filters): array * * Used for handling exceptions gracefully * - * @param array $args + * @param int $pageSize + * @param int $currentPage * @return SearchResult */ - private function createEmptyResult(array $args): SearchResult + private function createEmptyResult(int $pageSize, int $currentPage): SearchResult { return $this->searchResultFactory->create( [ 'totalCount' => 0, 'productsSearchResult' => [], - 'pageSize' => $args['pageSize'], - 'currentPage' => $args['currentPage'], + 'pageSize' => $pageSize, + 'currentPage' => $currentPage, 'totalPages' => 0, ] ); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index 4eb76fb5c2d5b..221a402cb2fff 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -12,7 +12,9 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Framework\Api\Search\SearchCriteriaInterface; -use Magento\Framework\Exception\InputException; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Api\SearchInterface; @@ -43,6 +45,11 @@ class Search implements ProductQueryInterface */ private $fieldSelection; + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @var ProductSearch */ @@ -60,6 +67,7 @@ class Search implements ProductQueryInterface * @param FieldSelection $fieldSelection * @param ProductSearch $productsProvider * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param ArgumentsProcessorInterface|null $argsSelection */ public function __construct( SearchInterface $search, @@ -67,7 +75,8 @@ public function __construct( PageSizeProvider $pageSize, FieldSelection $fieldSelection, ProductSearch $productsProvider, - SearchCriteriaBuilder $searchCriteriaBuilder + SearchCriteriaBuilder $searchCriteriaBuilder, + ArgumentsProcessorInterface $argsSelection = null ) { $this->search = $search; $this->searchResultFactory = $searchResultFactory; @@ -75,6 +84,8 @@ public function __construct( $this->fieldSelection = $fieldSelection; $this->productsProvider = $productsProvider; $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->argsSelection = $argsSelection ?: ObjectManager::getInstance() + ->get(ArgumentsProcessorInterface::class); } /** @@ -84,14 +95,13 @@ public function __construct( * @param ResolveInfo $info * @param ContextInterface $context * @return SearchResult - * @throws InputException + * @throws GraphQlInputException */ public function getResult( array $args, ResolveInfo $info, ContextInterface $context ): SearchResult { - $queryFields = $this->fieldSelection->getProductsFieldSelection($info); $searchCriteria = $this->buildSearchCriteria($args, $info); $realPageSize = $searchCriteria->getPageSize(); @@ -108,7 +118,7 @@ public function getResult( $searchResults = $this->productsProvider->getList( $searchCriteria, $itemsResults, - $queryFields, + $this->fieldSelection->getProductsFieldSelection($info), $context ); @@ -144,7 +154,8 @@ private function buildSearchCriteria(array $args, ResolveInfo $info): SearchCrit { $productFields = (array)$info->getFieldSelection(1); $includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']); - $searchCriteria = $this->searchCriteriaBuilder->build($args, $includeAggregations); + $processedArgs = $this->argsSelection->process((string) $info->fieldName, $args); + $searchCriteria = $this->searchCriteriaBuilder->build($processedArgs, $includeAggregations); return $searchCriteria; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php index 4b3e0a1a58dfd..4575c2013dc9c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php @@ -13,6 +13,9 @@ /** * Root category tree field resolver, used for GraphQL request processing. + * + * @deprecated Use the UID instead of a numeric id + * @see \Magento\CatalogGraphQl\Model\Resolver\RootCategoryUid */ class RootCategoryId implements ResolverInterface { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryUid.php new file mode 100644 index 0000000000000..9503e9f09b03c --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryUid.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Root category tree field resolver, used for GraphQL request processing. + */ +class RootCategoryUid implements ResolverInterface +{ + /** @var Uid */ + private $uidEncoder; + + /** + * Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + return $this->uidEncoder->encode((string) $context->getExtensionAttributes()->getStore()->getRootCategoryId()); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php index 992ab50467c72..b61ecfff4e3f1 100644 --- a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php +++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php @@ -106,6 +106,9 @@ private function getSearchableAttributes(): array $productAttributes->addFieldToFilter( ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'], [1, 1, [1, 2], 1] + )->setOrder( + 'position', + 'ASC' ); /** @var Attribute $attribute */ diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index fd3a834bff160..8c6fac0fe621c 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -67,6 +67,15 @@ </arguments> </type> + <type name="Magento\Framework\GraphQl\Query\Resolver\ArgumentsCompositeProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="category_uid" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUidArgsProcessor</item> + <item name="category_uids" xsi:type="object">Magento\CatalogGraphQl\Model\Category\CategoryUidsArgsProcessor</item> + </argument> + </arguments> + </type> + <type name="Magento\CatalogGraphQl\Plugin\Search\Request\ConfigReader"> <arguments> <argument name="exactMatchAttributes" xsi:type="array"> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 812965228682f..79281ff42cf26 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -83,27 +83,28 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M } interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes.") { - id: Int @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") + id: Int @deprecated(reason: "Use the `uid` field instead.") @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId") + uid: ID! @doc(description: "The unique ID for a `ProductInterface` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToUid") name: String @doc(description: "The product name. Customers use this name to identify the product.") sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") special_price: Float @doc(description: "The discounted price of the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\SpecialPrice") - special_from_date: String @doc(description: "The beginning date that a product has a special price.") + special_from_date: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The beginning date that a product has a special price.") special_to_date: String @doc(description: "The end date that a product has a special price.") - attribute_set_id: Int @doc(description: "The attribute set assigned to the product.") + attribute_set_id: Int @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The attribute set assigned to the product.") meta_title: String @doc(description: "A string that is displayed in the title bar and tab of the browser and in search results lists.") meta_keyword: String @doc(description: "A comma-separated list of keywords that are visible only to search engines.") meta_description: String @doc(description: "A brief overview of the product for search results listings, maximum 255 characters.") image: ProductImage @doc(description: "The relative path to the main image on the product page.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") small_image: ProductImage @doc(description: "The relative path to the small image, which is used on catalog pages.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage") - new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") - new_to_date: String @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") + new_from_date: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") + new_to_date: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo") tier_price: Float @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.") options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page.") - created_at: String @doc(description: "Timestamp indicating when the product was created.") - updated_at: String @doc(description: "Timestamp indicating when the product was updated.") + created_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the product was created.") + updated_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the product was updated.") country_of_manufacture: String @doc(description: "The product's country of origin.") type_id: String @doc(description: "One of simple, virtual, bundle, downloadable, grouped, or configurable.") @deprecated(reason: "Use __typename instead.") websites: [Website] @doc(description: "An array of websites in which the product is available.") @deprecated(reason: "The field should not be used on the storefront.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites") @@ -132,7 +133,7 @@ type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableAreaValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") } type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation.") { @@ -154,7 +155,7 @@ type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price: Float @doc(description: "The price assigned to this option.") price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableDateValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") } type CustomizableDropDownOption implements CustomizableOptionInterface @doc(description: "CustomizableDropDownOption contains information about a drop down menu that is defined as part of a customizable option.") { @@ -168,7 +169,7 @@ type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableDropDownValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option.") { @@ -182,7 +183,7 @@ type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") + uid: ID! @doc(description: "The unique ID for a `CustomizableMultipleValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option.") { @@ -195,7 +196,7 @@ type CustomizableFieldValue @doc(description: "CustomizableFieldValue defines th price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableFieldValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") } type CustomizableFileOption implements CustomizableOptionInterface @doc(description: "CustomizableFileOption contains information about a file picker that is defined as part of a customizable option.") { @@ -210,7 +211,7 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the file_extension: String @doc(description: "The file extension to accept.") image_size_x: Int @doc(description: "The maximum width of an image.") image_size_y: Int @doc(description: "The maximum height of an image.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableFileValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") } interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") { @@ -231,7 +232,8 @@ interface CustomizableOptionInterface @typeResolver(class: "Magento\\CatalogGrap title: String @doc(description: "The display name for this option.") required: Boolean @doc(description: "Indicates whether the option is required.") sort_order: Int @doc(description: "The order in which the option is displayed.") - option_id: Int @doc(description: "Option ID.") + option_id: Int @deprecated(reason: "Use `uid` instead") @doc(description: "Option ID.") + uid: ID! @doc(description: "The unique ID for a `CustomizableOptionInterface` object.") } interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "CustomizableProductInterface contains information about customizable product options.") { @@ -239,7 +241,8 @@ interface CustomizableProductInterface @typeResolver(class: "Magento\\CatalogGra } interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\CategoryInterfaceTypeResolver") @doc(description: "CategoryInterface contains the full set of attributes that can be returned in a category search.") { - id: Int @doc(description: "An ID that uniquely identifies the category.") + id: Int @deprecated(reason: "Use the `uid` argument instead.") @doc(description: "An ID that uniquely identifies the category.") + uid: ID! @doc(description: "The unique ID for a `CategoryInterface` object.") description: String @doc(description: "An optional description of the category.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryHtmlAttribute") name: String @doc(description: "The display name of the category.") path: String @doc(description: "Category Path.") @@ -249,8 +252,8 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model canonical_url: String @doc(description: "Relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Categories' is enabled") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CanonicalUrl") position: Int @doc(description: "The position of the category relative to other categories at the same level in tree.") level: Int @doc(description: "Indicates the depth of the category within the tree.") - created_at: String @doc(description: "Timestamp indicating when the category was created.") - updated_at: String @doc(description: "Timestamp indicating when the category was updated.") + created_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the category was created.") + updated_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the category was updated.") product_count: Int @doc(description: "The number of products in the category that are marked as visible. By default, in complex products, parent products are visible, but their child products are not.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\ProductsCount") default_sort_by: String @doc(description: "The attribute to use for sorting.") products( @@ -261,8 +264,9 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs") } -type Breadcrumb @doc(description: "Breadcrumb item."){ - category_id: Int @doc(description: "Category ID.") +type Breadcrumb @doc(description: "Breadcrumb item.") { + category_id: Int @deprecated(reason: "Use the `category_uid` argument instead.") @doc(description: "Category ID.") + category_uid: ID! @doc(description: "The unique ID for a `Breadcrumb` object.") category_name: String @doc(description: "Category name.") category_level: Int @doc(description: "Category level.") category_url_key: String @doc(description: "Category URL key.") @@ -280,7 +284,7 @@ type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines th sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the radio button is displayed.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableRadioValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option.") { @@ -294,7 +298,7 @@ type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the checkbox value is displayed.") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `CustomizableCheckboxValue` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory.") { @@ -320,13 +324,15 @@ type CategoryProducts @doc(description: "The category products object returned i } input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { - category_id: FilterEqualTypeInput @doc(description: "Filter product by category id") + category_id: FilterEqualTypeInput @deprecated(reason: "Use the `category_uid` argument instead.") @doc(description: "Deprecated: use `category_uid` to filter product by category id.") + category_uid: FilterEqualTypeInput @doc(description: "Filter product by the unique ID for a `CategoryInterface` object.") } input CategoryFilterInput @doc(description: "CategoryFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { - ids: FilterEqualTypeInput @doc(description: "Filter by category ID that uniquely identifies the category.") - parent_id: FilterEqualTypeInput @doc(description: "Filter by parent category ID") + ids: FilterEqualTypeInput @deprecated(reason: "Use the `category_uid` argument instead.") @doc(description: "Deprecated: use 'category_uid' to filter uniquely identifiers of categories.") + category_uid: FilterEqualTypeInput @doc(description: "Filter by the unique category ID for a `CategoryInterface` object.") + parent_id: FilterEqualTypeInput @doc(description: "Filter by the unique parent category ID for a `CategoryInterface` object.") url_key: FilterEqualTypeInput @doc(description: "Filter by the part of the URL that identifies the category.") name: FilterMatchTypeInput @doc(description: "Filter by the display name of the category.") url_path: FilterEqualTypeInput @doc(description: "Filter by the URL path for the category.") @@ -426,7 +432,8 @@ input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput spe } type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") { - id: Int @doc(description: "The identifier assigned to the object.") + id: Int @deprecated(reason: "Use `uid` instead.") @doc(description: "The identifier assigned to the object.") + uid: ID! @doc(description: "The unique ID for a `MediaGalleryEntry` object.") media_type: String @doc(description: "image or video.") label: String @doc(description: "The alt text displayed on the UI when the user points to the image.") position: Int @doc(description: "The media item's position after it has been sorted.") @@ -454,7 +461,7 @@ type LayerFilterItem implements LayerFilterItemInterface { } -type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category ID, and custom attributes).") { +type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category `UID`, and custom attributes).") { count: Int @doc(description: "The number of options in the aggregation group.") label: String @doc(description: "The aggregation display name.") attribute_code: String! @doc(description: "Attribute code of the aggregation group.") @@ -491,7 +498,8 @@ type StoreConfig @doc(description: "The type contains information about a store grid_per_page : Int @doc(description: "Products per Page on Grid Default Value.") list_per_page : Int @doc(description: "Products per Page on List Default Value.") catalog_default_sort_by : String @doc(description: "Default Sort By.") - root_category_id: Int @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId") + root_category_id: Int @deprecated(reason: "Use `root_category_uid` instead") @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId") + root_category_uid: ID @doc(description: "The unique ID for a `CategoryInterface` object.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryUid") } type SimpleWishlistItem implements WishlistItemInterface @doc(description: "A simple product wish list Item") { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 428961aa6ddf6..673dbcb3b3c99 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1807,7 +1807,7 @@ protected function _saveProducts() if ($column === self::COL_MEDIA_IMAGE) { $rowData[$column][] = $uploadedFile; } - $mediaGallery[$storeId][$rowSku][$uploadedFile] = [ + $mediaGalleryStoreData = [ 'attribute_id' => $this->getMediaGalleryAttributeId(), 'label' => isset($rowLabels[$column][$columnImageKey]) ? $rowLabels[$column][$columnImageKey] @@ -1817,6 +1817,15 @@ protected function _saveProducts() ? $imageHiddenStates[$columnImage] : '0', 'value' => $uploadedFile, ]; + $mediaGallery[$storeId][$rowSku][$uploadedFile] = $mediaGalleryStoreData; + // Add record for default scope if it does not exist + if (!($mediaGallery[Store::DEFAULT_STORE_ID][$rowSku][$uploadedFile] ?? [])) { + //Set label and disabled values to their default values + $mediaGalleryStoreData['label'] = null; + $mediaGalleryStoreData['disabled'] = 0; + $mediaGallery[Store::DEFAULT_STORE_ID][$rowSku][$uploadedFile] = $mediaGalleryStoreData; + } + } } } @@ -1965,7 +1974,7 @@ private function getAlreadyExistedImage(array $imageRow, string $columnImage, st if (filter_var($columnImage, FILTER_VALIDATE_URL)) { $hash = $this->getFileHash($columnImage); } else { - $path = $importDir . DS . $columnImage; + $path = $importDir . DIRECTORY_SEPARATOR . $columnImage; $hash = $this->isFileExists($path) ? $this->getFileHash($path) : ''; } @@ -1991,7 +2000,7 @@ function ($exists, $file) use ($hash) { private function addImageHashes(array &$images): void { $productMediaPath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) - ->getAbsolutePath(DS . 'catalog' . DS . 'product'); + ->getAbsolutePath(DIRECTORY_SEPARATOR . 'catalog' . DIRECTORY_SEPARATOR . 'product'); foreach ($images as $storeId => $skus) { foreach ($skus as $sku => $files) { @@ -2188,7 +2197,7 @@ private function getImportDir(): string $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; return empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]) - ? $dirAddon . DS . $this->_mediaDirectory->getRelativePath('import') + ? $dirAddon . DIRECTORY_SEPARATOR . $this->_mediaDirectory->getRelativePath('import') : $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index d4694b72ba64f..c838688c1c4f8 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -12,6 +12,7 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\Store\Model\Store; /** * Process and saves images during import. @@ -259,7 +260,10 @@ private function prepareMediaGalleryValueData( $position = $data['position']; $storeId = $data['store_id']; $mediaGalleryValueData[$index]['value_id'] = $productIdMediaValueIdMap[$productId][$value]; - $mediaGalleryValueData[$index]['position'] = $position + ($lastPositions[$storeId][$productId] ?? 0); + $lastPosition = $lastPositions[$storeId][$productId] + ?? $lastPositions[Store::DEFAULT_STORE_ID][$productId] + ?? 0; + $mediaGalleryValueData[$index]['position'] = $position + $lastPosition; unset($mediaGalleryValueData[$index]['value']); } return $mediaGalleryValueData; diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php index 2ad7ca9f14963..2b37c9099a0e6 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php @@ -105,7 +105,7 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = } if (!empty($entityIds)) { - $select->where('stock_item.product_id in (?)', $entityIds, \Zend_Db::INT_TYPE); + $select->where('stock_item.product_id IN (?)', $entityIds, \Zend_Db::INT_TYPE); } $select->group('stock_item.product_id'); @@ -121,7 +121,7 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = foreach ($batchSelectIterator as $select) { $productIds = null; foreach ($connection->query($select)->fetchAll() as $row) { - $productIds[] = $row['product_id']; + $productIds[] = (int) $row['product_id']; } if ($productIds !== null) { $where = [$priceTable->getEntityField() .' IN (?)' => $productIds]; diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php index 4ea6b6bcfde9a..0b5f248331bfd 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php @@ -167,7 +167,7 @@ public function getRelationsByChild($childIds) )->join( ['relation' => $this->_getTable('catalog_product_relation')], 'relation.parent_id = cpe.' . $linkField - )->where('child_id IN(?)', $childIds); + )->where('child_id IN(?)', $childIds, \Zend_Db::INT_TYPE); return $connection->fetchCol($select); } @@ -262,7 +262,7 @@ private function doReindex($productIds = []) // retrieve product types by processIds $select = $connection->select() ->from($this->_getTable('catalog_product_entity'), ['entity_id', 'type_id']) - ->where('entity_id IN(?)', $productIds); + ->where('entity_id IN(?)', $productIds, \Zend_Db::INT_TYPE); $pairs = $connection->fetchPairs($select); $byType = []; diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php index 43a5aabee9779..f85f3f3576279 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php @@ -149,13 +149,21 @@ public function execute($ids = null): void $select = $connection->select(); $select->distinct(true); - $select->from(['e' => $entityMetadata->getEntityTable()], $entityMetadata->getIdentifierField()); + $select->from( + [ + 'e' => $entityMetadata->getEntityTable() + ], + $entityMetadata->getIdentifierField() + )->where( + 'type_id = ?', + $indexer->getTypeId() + ); $batchQueries = $this->batchQueryGenerator->generate( $entityMetadata->getIdentifierField(), $select, $batchRowCount, - BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR + BatchIteratorInterface::UNIQUE_FIELD_ITERATOR ); foreach ($batchQueries as $query) { diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php index c151e5897abd5..dec18044b699e 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php @@ -261,7 +261,7 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f $select->columns(['status' => $this->getStatusExpression($connection, true)]); if ($entityIds !== null) { - $select->where('e.entity_id IN(?)', $entityIds); + $select->where('e.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } return $select; diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index 02e443d09b228..afb7d51335df8 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -153,7 +153,7 @@ public function getProductsStockStatuses($productIds, $websiteId, $stockId = Sto $select = $this->getConnection()->select() ->from($this->getMainTable(), ['product_id', 'stock_status']) - ->where('product_id IN(?)', $productIds) + ->where('product_id IN(?)', $productIds, \Zend_Db::INT_TYPE) ->where('stock_id=?', (int) $stockId) ->where('website_id=?', (int) $websiteId); return $this->getConnection()->fetchPairs($select); @@ -190,7 +190,8 @@ public function getProductsType($productIds) ['entity_id', 'type_id'] )->where( 'entity_id IN(?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); return $this->getConnection()->fetchPairs($select); } @@ -360,7 +361,8 @@ public function getProductStatus($productIds, $storeId = null) $attribute->getAttributeId() )->where( "t1.{$linkField} IN(?)", - $productIds + $productIds, + \Zend_Db::INT_TYPE ); $rows = $connection->fetchPairs($select); diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index 2dd47eae16959..9351abf0ead3d 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -64,6 +64,14 @@ class MassUpdateProductAttribute * @var ProductRepositoryInterface */ private $productRepository; + + /** + * @var array + */ + private $useConfigFieldMap = [ + 'enable_qty_increments' => 'use_config_enable_qty_inc' + ]; + /** * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper @@ -146,7 +154,9 @@ private function addConfigSettings($inventoryData) { $options = $this->stockConfiguration->getConfigItemOptions(); foreach ($options as $option) { - $useConfig = 'use_config_' . $option; + $useConfig = isset($this->useConfigFieldMap[$option]) + ? $this->useConfigFieldMap[$option] + : 'use_config_' . $option; if (isset($inventoryData[$option]) && !isset($inventoryData[$useConfig])) { $inventoryData[$useConfig] = 0; } diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php index 0f3d8be212e30..090a74afca050 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php @@ -82,7 +82,7 @@ public function testModifyPrice() $connectionMock->expects($this->once())->method('select')->willReturn($selectMock); $selectMock->expects($this->at(2)) ->method('where') - ->with('stock_item.product_id in (?)', $entityIds) + ->with('stock_item.product_id IN (?)', $entityIds) ->willReturn($selectMock); $this->generator->expects($this->once()) ->method('generate') diff --git a/app/code/Magento/CatalogInventory/etc/mview.xml b/app/code/Magento/CatalogInventory/etc/mview.xml index 338f1fe0610a1..9733fa32583f1 100644 --- a/app/code/Magento/CatalogInventory/etc/mview.xml +++ b/app/code/Magento/CatalogInventory/etc/mview.xml @@ -11,6 +11,7 @@ <table name="cataloginventory_stock_item" entity_column="product_id" /> <!--Track product status to trigger stock indexer--> <table name="catalog_product_entity_int" entity_column="entity_id" /> + <table name="catalog_product_link" entity_column="product_id" /> </subscriptions> </view> <view id="catalog_product_price" class="Magento\Catalog\Model\Indexer\Product\Price" group="indexer"> diff --git a/app/code/Magento/CatalogRule/Model/ResourceModel/Rule.php b/app/code/Magento/CatalogRule/Model/ResourceModel/Rule.php index 662cdede84ede..dd4f3306b8603 100644 --- a/app/code/Magento/CatalogRule/Model/ResourceModel/Rule.php +++ b/app/code/Magento/CatalogRule/Model/ResourceModel/Rule.php @@ -187,7 +187,7 @@ public function getRulePrices(\DateTimeInterface $date, $websiteId, $customerGro ->where('rule_date = ?', $date->format('Y-m-d')) ->where('website_id = ?', $websiteId) ->where('customer_group_id = ?', $customerGroupId) - ->where('product_id IN(?)', $productIds); + ->where('product_id IN(?)', $productIds, \Zend_Db::INT_TYPE); return $connection->fetchPairs($select); } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml index fb218297b646d..3c08fbdf641e4 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> - <!-- Perform reindex and flush cache --> - <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + <comment userInput="Adding the comment to replace AdminReindexAndFlushCache action group ('indexer:reindex', 'cache:flush' commands) for preserving Backward Compatibility" stepKey="reindexAndFlushCache"/> </before> <after> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml index b90cc66a10d68..a40bf63c5a388 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml @@ -38,7 +38,9 @@ <!-- Delete products and category --> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml index 9f793d5c8c393..106e0ffabb2b2 100644 --- a/app/code/Magento/CatalogRule/etc/mview.xml +++ b/app/code/Magento/CatalogRule/etc/mview.xml @@ -21,6 +21,7 @@ <table name="catalog_product_entity_tier_price" entity_column="entity_id" /> <table name="catalog_product_entity_varchar" entity_column="entity_id" /> <table name="catalog_category_product" entity_column="product_id" /> + <table name="catalog_product_link" entity_column="product_id" /> </subscriptions> </view> <view id="catalog_product_price" class="Magento\Catalog\Model\Indexer\Product\Price" group="indexer"> diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml index 48f53da8e2a2e..ca9017b7c5f29 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml +++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithAssignedSimpleProducts2Test.xml @@ -190,7 +190,7 @@ </actionGroup> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + <comment userInput="Adding the comment to replace AdminReindexAndFlushCache action group ('indexer:reindex', 'cache:flush' commands) for preserving Backward Compatibility" stepKey="reindexAndFlushCache"/> <!-- Login to storefront from customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomerOnStorefront"> diff --git a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml index 350f896606c19..b20bd34106e03 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml +++ b/app/code/Magento/CatalogRuleConfigurable/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithOptions2Test.xml @@ -173,7 +173,7 @@ </actionGroup> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplySecondPriceRule"/> - <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + <comment userInput="Adding the comment to replace AdminReindexAndFlushCache action group ('indexer:reindex', 'cache:flush' commands) for preserving Backward Compatibility" stepKey="reindexAndFlushCache"/> <!-- Assert product in storefront product page --> <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="amOnProductPage"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php new file mode 100644 index 0000000000000..fbacddac1ce02 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogUrlRewrite\Model\ResourceModel\Product\GetUrlRewriteData; +use Magento\Store\Model\Store; + +/** + * Product data needed for url rewrite generation locator class + */ +class GetProductUrlRewriteDataByStore +{ + /** + * @var array + */ + private $urlRewriteData = []; + + /** + * @var GetUrlRewriteData + */ + private $getUrlRewriteData; + + /** + * @param GetUrlRewriteData $getUrlRewriteData + */ + public function __construct(GetUrlRewriteData $getUrlRewriteData) + { + $this->getUrlRewriteData = $getUrlRewriteData; + } + + /** + * Retrieves data for product by store + * + * @param ProductInterface $product + * @param int $storeId + * @return array + */ + public function execute(ProductInterface $product, int $storeId): array + { + $productId = $product->getId(); + if (isset($this->urlRewriteData[$productId][$storeId])) { + return $this->urlRewriteData[$productId][$storeId]; + } + if (empty($this->urlRewriteData[$productId])) { + $storesData = $this->getUrlRewriteData->execute($product); + foreach ($storesData as $storeData) { + $this->urlRewriteData[$productId][$storeData['store_id']] = [ + 'visibility' => (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']), + 'url_key' => $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'], + ]; + } + } + + if (!isset($this->urlRewriteData[$productId][$storeId])) { + $this->urlRewriteData[$productId][$storeId] = $this->urlRewriteData[$productId][Store::DEFAULT_STORE_ID]; + } + + return $this->urlRewriteData[$productId][$storeId]; + } + + /** + * Clears product url rewrite data in local cache + * + * @param ProductInterface $product + */ + public function clearProductUrlRewriteDataCache(ProductInterface $product) + { + unset($this->urlRewriteData[$product->getId()]); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Products/AppendUrlRewritesToProducts.php b/app/code/Magento/CatalogUrlRewrite/Model/Products/AppendUrlRewritesToProducts.php new file mode 100644 index 0000000000000..15d4aabf4246b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/Products/AppendUrlRewritesToProducts.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model\Products; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogUrlRewrite\Model\Product\GetProductUrlRewriteDataByStore; +use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; +use Magento\Store\Model\Store; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Model\UrlPersistInterface; + +/** + * Update existing url rewrites or create new ones if needed + */ +class AppendUrlRewritesToProducts +{ + /** + * @var ProductUrlRewriteGenerator + */ + private $productUrlRewriteGenerator; + + /** + * @var StoreViewService + */ + private $storeViewService; + + /** + * @var ProductUrlPathGenerator + */ + private $productUrlPathGenerator; + + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var GetProductUrlRewriteDataByStore + */ + private $getDataByStore; + + /** + * @param ProductUrlRewriteGenerator $urlRewriteGenerator + * @param StoreViewService $storeViewService + * @param ProductUrlPathGenerator $urlPathGenerator + * @param UrlPersistInterface $urlPersist + * @param GetProductUrlRewriteDataByStore $getDataByStore + */ + public function __construct( + ProductUrlRewriteGenerator $urlRewriteGenerator, + StoreViewService $storeViewService, + ProductUrlPathGenerator $urlPathGenerator, + UrlPersistInterface $urlPersist, + GetProductUrlRewriteDataByStore $getDataByStore + ) { + $this->productUrlRewriteGenerator = $urlRewriteGenerator; + $this->storeViewService = $storeViewService; + $this->productUrlPathGenerator = $urlPathGenerator; + $this->urlPersist = $urlPersist; + $this->getDataByStore = $getDataByStore; + } + + /** + * Update existing rewrites and add for specific stores websites + * + * @param ProductInterface[] $products + * @param array $storesToAdd + * @throws UrlAlreadyExistsException + */ + public function execute(array $products, array $storesToAdd): void + { + foreach ($products as $product) { + $forceGenerateDefault = false; + foreach ($storesToAdd as $storeId) { + if ($this->needGenerateUrlForStore($product, (int)$storeId)) { + $urls[] = $this->generateUrls($product, (int)$storeId); + } elseif ((int)$product->getStoreId() !== Store::DEFAULT_STORE_ID) { + $forceGenerateDefault = true; + } + } + if ($product->getStoreId() === Store::DEFAULT_STORE_ID + || $this->isProductAssignedToStore($product)) { + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); + $urls[] = $this->productUrlRewriteGenerator->generate($product); + } + if ($forceGenerateDefault && $product->getStoreId() !== Store::DEFAULT_STORE_ID) { + $urls[] = $this->generateUrls($product, Store::DEFAULT_STORE_ID); + } + $this->getDataByStore->clearProductUrlRewriteDataCache($product); + } + if (!empty($urls)) { + $this->urlPersist->replace(array_merge(...$urls)); + } + } + + /** + * Generate urls for specific store + * + * @param ProductInterface $product + * @param int $storeId + * @return array + */ + private function generateUrls(ProductInterface $product, int $storeId): array + { + $storeData = $this->getDataByStore->execute($product, $storeId); + $origStoreId = $product->getStoreId(); + $origVisibility = $product->getVisibility(); + $origUrlKey = $product->getUrlKey(); + $product->setStoreId($storeId); + $product->setVisibility($storeData['visibility'] ?? Visibility::VISIBILITY_NOT_VISIBLE); + $product->setUrlKey($storeData['url_key'] ?? ''); + $product->unsUrlPath(); + $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); + $urls = $this->productUrlRewriteGenerator->generate($product); + $product->setStoreId($origStoreId); + $product->setVisibility($origVisibility); + $product->setUrlKey($origUrlKey); + + return $urls; + } + + /** + * Does product has scope overridden url key value + * + * @param ProductInterface $product + * @param int $storeId + * @return bool + */ + private function needGenerateUrlForStore(ProductInterface $product, int $storeId): bool + { + return (int)$product->getStoreId() !== $storeId + && $this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( + $storeId, + $product->getId(), + Product::ENTITY + ); + } + + /** + * Is product still assigned to store which request is performed from + * + * @param ProductInterface $product + * @return bool + */ + private function isProductAssignedToStore(ProductInterface $product): bool + { + return in_array($product->getStoreId(), $product->getStoreIds()); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Product/GetUrlRewriteData.php b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Product/GetUrlRewriteData.php new file mode 100644 index 0000000000000..f4cef73a040e8 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/ResourceModel/Product/GetUrlRewriteData.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model\ResourceModel\Product; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Fetch product url rewrite data from database + */ +class GetUrlRewriteData +{ + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var Config + */ + private $eavConfig; + + /** + * @param MetadataPool $metadataPool + * @param ResourceConnection $connection + * @param Config $eavConfig + */ + public function __construct( + MetadataPool $metadataPool, + ResourceConnection $connection, + Config $eavConfig + ) { + $this->metadataPool = $metadataPool; + $this->resource = $connection; + $this->eavConfig = $eavConfig; + } + + /** + * Fetches product store data required for url key generation + * + * @param ProductInterface $product + * @return array + */ + public function execute(ProductInterface $product): array + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); + $connection = $this->resource->getConnection(); + $visibilityAttribute = $this->eavConfig->getAttribute(Product::ENTITY, 'visibility'); + $urlKeyAttribute = $this->eavConfig->getAttribute(Product::ENTITY, 'url_key'); + $visibilitySelect = $connection->select() + ->from(['visibility' => $visibilityAttribute->getBackendTable()]) + ->joinRight( + ['url_key' => $urlKeyAttribute->getBackendTable()], + 'url_key.' . $linkField . ' = visibility.' . $linkField . ' AND url_key.store_id = visibility.store_id' + . ' AND url_key.attribute_id = ' . $urlKeyAttribute->getId(), + ['url_key.value as url_key'] + ) + ->reset(Select::COLUMNS) + ->columns(['url_key.store_id', 'url_key.value AS url_key', 'visibility.value AS visibility']) + ->where('visibility.' . $linkField . ' = ?', $product->getData($linkField)) + ->where('visibility.attribute_id = ?', $visibilityAttribute->getId()); + $urlKeySelect = $connection->select() + ->from(['url_key' => $urlKeyAttribute->getBackendTable()]) + ->joinLeft( + ['visibility' => $visibilityAttribute->getBackendTable()], + 'url_key.' . $linkField . ' = visibility.' . $linkField . ' AND url_key.store_id = visibility.store_id' + . ' AND visibility.attribute_id = ' . $visibilityAttribute->getId(), + ['visibility.value as visibility'] + ) + ->reset(Select::COLUMNS) + ->columns(['url_key.store_id', 'url_key.value AS url_key', 'visibility.value as visibility']) + ->where('url_key.' . $linkField . ' = ?', $product->getData($linkField)) + ->where('url_key.attribute_id = ?', $urlKeyAttribute->getId()); + + $select = $connection->select()->union([$visibilitySelect, $urlKeySelect], Select::SQL_UNION); + + return $connection->fetchAll($select); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php index 6eda8dd0b61ee..512340354172e 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserver.php @@ -3,73 +3,156 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Observer; use Magento\Catalog\Model\Product; -use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator; +use Magento\Catalog\Model\Product\Visibility; +use Magento\CatalogUrlRewrite\Model\Products\AppendUrlRewritesToProducts; use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -use Magento\Framework\App\ObjectManager; -use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreResolver\GetStoresListByWebsiteIds; +use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; /** * Class ProductProcessUrlRewriteSavingObserver + * + * Generates urls for product url rewrites */ class ProductProcessUrlRewriteSavingObserver implements ObserverInterface { /** - * @var ProductUrlRewriteGenerator + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var AppendUrlRewritesToProducts */ - private $productUrlRewriteGenerator; + private $appendRewrites; /** - * @var UrlPersistInterface + * @var ScopeConfigInterface */ - private $urlPersist; + private $scopeConfig; /** - * @var ProductUrlPathGenerator + * @var GetStoresListByWebsiteIds */ - private $productUrlPathGenerator; + private $getStoresList; /** - * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator * @param UrlPersistInterface $urlPersist - * @param ProductUrlPathGenerator|null $productUrlPathGenerator + * @param AppendUrlRewritesToProducts|null $appendRewrites + * @param ScopeConfigInterface $scopeConfig + * @param GetStoresListByWebsiteIds $getStoresList */ public function __construct( - ProductUrlRewriteGenerator $productUrlRewriteGenerator, UrlPersistInterface $urlPersist, - ProductUrlPathGenerator $productUrlPathGenerator = null + AppendUrlRewritesToProducts $appendRewrites, + ScopeConfigInterface $scopeConfig, + GetStoresListByWebsiteIds $getStoresList ) { - $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; $this->urlPersist = $urlPersist; - $this->productUrlPathGenerator = $productUrlPathGenerator ?: ObjectManager::getInstance() - ->get(ProductUrlPathGenerator::class); + $this->appendRewrites = $appendRewrites; + $this->scopeConfig = $scopeConfig; + $this->getStoresList = $getStoresList; } /** * Generate urls for UrlRewrite and save it in storage * - * @param \Magento\Framework\Event\Observer $observer + * @param Observer $observer * @return void - * @throws \Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException + * @throws UrlAlreadyExistsException */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { /** @var Product $product */ $product = $observer->getEvent()->getProduct(); - if ($product->dataHasChangedFor('url_key') - || $product->getIsChangedCategories() - || $product->getIsChangedWebsites() - || $product->dataHasChangedFor('visibility') - ) { - if ($product->isVisibleInSiteVisibility()) { - $product->unsUrlPath(); - $product->setUrlPath($this->productUrlPathGenerator->getUrlPath($product)); - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); - } + if ($this->isNeedUpdateRewrites($product)) { + $this->deleteObsoleteRewrites($product); + $oldWebsiteIds = $product->getOrigData('website_ids') ?? []; + $storesToAdd = $this->getStoresList->execute( + array_diff($product->getWebsiteIds(), $oldWebsiteIds) + ); + $this->appendRewrites->execute([$product], $storesToAdd); } } + + /** + * Remove obsolete Url rewrites + * + * @param Product $product + */ + private function deleteObsoleteRewrites(Product $product): void + { + //do not perform redundant delete request for new product + if ($product->getOrigData('entity_id') === null) { + return; + } + $oldWebsiteIds = $product->getOrigData('website_ids') ?? []; + $storesToRemove = $this->getStoresList->execute( + array_diff($oldWebsiteIds, $product->getWebsiteIds()) + ); + if ((int)$product->getVisibility() === Visibility::VISIBILITY_NOT_VISIBLE) { + $isGlobalScope = $product->getStoreId() == Store::DEFAULT_STORE_ID; + $storesToRemove[] = $isGlobalScope ? $product->getStoreIds() : $product->getStoreId(); + } + if ($storesToRemove) { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $product->getId(), + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storesToRemove, + ] + ); + } + } + + /** + * Is website assignment updated + * + * @param Product $product + * @return bool + */ + private function isWebsiteChanged(Product $product) + { + $oldWebsiteIds = $product->getOrigData('website_ids'); + $newWebsiteIds = $product->getWebsiteIds(); + + return array_diff($oldWebsiteIds, $newWebsiteIds) || array_diff($newWebsiteIds, $oldWebsiteIds); + } + + + /** + * Is product rewrites need to be updated + * + * @param Product $product + * @return bool + */ + private function isNeedUpdateRewrites(Product $product): bool + { + return ($product->dataHasChangedFor('url_key') + && (int)$product->getVisibility() !== Visibility::VISIBILITY_NOT_VISIBLE) + || ($product->getIsChangedCategories() && $this->isGenerateCategoryProductRewritesEnabled()) + || $this->isWebsiteChanged($product) + || $product->dataHasChangedFor('visibility'); + } + + /** + * Return product use category path in rewrite config value + * + * @return bool + */ + private function isGenerateCategoryProductRewritesEnabled(): bool + { + return $this->scopeConfig->isSetFlag('catalog/seo/generate_category_product_rewrites'); + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php deleted file mode 100644 index 44b47faf3d4b8..0000000000000 --- a/app/code/Magento/CatalogUrlRewrite/Observer/ProductToWebsiteChangeObserver.php +++ /dev/null @@ -1,104 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\CatalogUrlRewrite\Observer; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Product\Visibility; -use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\Event\ObserverInterface; -use Magento\Store\Model\Store; -use Magento\UrlRewrite\Model\UrlPersistInterface; -use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\Store\Api\StoreWebsiteRelationInterface; -use Magento\Framework\App\ObjectManager; - -/** - * Observer to assign the products to website - */ -class ProductToWebsiteChangeObserver implements ObserverInterface -{ - /** - * @var ProductUrlRewriteGenerator - */ - protected $productUrlRewriteGenerator; - - /** - * @var UrlPersistInterface - */ - protected $urlPersist; - - /** - * @var ProductRepositoryInterface - */ - protected $productRepository; - - /** - * @var RequestInterface - */ - protected $request; - - /** - * @var StoreWebsiteRelationInterface - */ - private $storeWebsiteRelation; - - /** - * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator - * @param UrlPersistInterface $urlPersist - * @param ProductRepositoryInterface $productRepository - * @param RequestInterface $request - * @param StoreWebsiteRelationInterface $storeWebsiteRelation - */ - public function __construct( - ProductUrlRewriteGenerator $productUrlRewriteGenerator, - UrlPersistInterface $urlPersist, - ProductRepositoryInterface $productRepository, - RequestInterface $request, - StoreWebsiteRelationInterface $storeWebsiteRelation = null - ) { - $this->productUrlRewriteGenerator = $productUrlRewriteGenerator; - $this->urlPersist = $urlPersist; - $this->productRepository = $productRepository; - $this->request = $request; - $this->storeWebsiteRelation = $storeWebsiteRelation ?: - ObjectManager::getInstance()->get(StoreWebsiteRelationInterface::class); - } - - /** - * Generate urls for UrlRewrite and save it in storage - * - * @param \Magento\Framework\Event\Observer $observer - * @return void - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - foreach ($observer->getEvent()->getProducts() as $productId) { - $product = $this->productRepository->getById( - $productId, - false, - $this->request->getParam('store_id', Store::DEFAULT_STORE_ID) - ); - - if (!empty($this->productUrlRewriteGenerator->generate($product))) { - if ($this->request->getParam('remove_website_ids')) { - foreach ($this->request->getParam('remove_website_ids') as $webId) { - foreach ($this->storeWebsiteRelation->getStoreByWebsiteId($webId) as $storeId) { - $this->urlPersist->deleteByData([ - UrlRewrite::ENTITY_ID => $product->getId(), - UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, - UrlRewrite::STORE_ID => $storeId - ]); - } - } - } - if ($product->getVisibility() != Visibility::VISIBILITY_NOT_VISIBLE) { - $this->urlPersist->replace($this->productUrlRewriteGenerator->generate($product)); - } - } - } - } -} diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php new file mode 100644 index 0000000000000..f9c605ab489a5 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewrites.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Plugin\Catalog\Model\Product; + +use Magento\Catalog\Model\Product\Action as ProductAction; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\CatalogUrlRewrite\Model\Products\AppendUrlRewritesToProducts; +use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\Store\Api\StoreWebsiteRelationInterface; +use Magento\Store\Model\StoreResolver\GetStoresListByWebsiteIds; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; + +/** + * Update URL rewrites after website change + */ +class UpdateProductWebsiteUrlRewrites +{ + /** + * @var UrlPersistInterface + */ + private $urlPersist; + + /** + * @var Collection + */ + private $productCollection; + + /** + * @var AppendUrlRewritesToProducts + */ + private $appendRewrites; + + /** + * @var GetStoresListByWebsiteIds + */ + private $getStoresList; + + /** + * @param UrlPersistInterface $urlPersist + * @param Collection $productCollection + * @param AppendUrlRewritesToProducts $appendRewrites + * @param GetStoresListByWebsiteIds $getStoresList + */ + public function __construct( + UrlPersistInterface $urlPersist, + Collection $productCollection, + AppendUrlRewritesToProducts $appendRewrites, + GetStoresListByWebsiteIds $getStoresList + ) { + $this->urlPersist = $urlPersist; + $this->productCollection = $productCollection; + $this->appendRewrites = $appendRewrites; + $this->getStoresList = $getStoresList; + } + + /** + * Update url rewrites after website changes + * + * @param ProductAction $subject + * @param void $result + * @param array $productIds + * @param array $websiteIds + * @param string $type + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterUpdateWebsites( + ProductAction $subject, + $result, + array $productIds, + array $websiteIds, + string $type + ): void { + if (empty($websiteIds)) { + return; + } + $storeIds = $this->getStoresList->execute($websiteIds); + // Remove the URLs from websites this product no longer belongs to + if ($type == 'remove') { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $productIds, + UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::STORE_ID => $storeIds, + ] + ); + } else { + $collection = $this->productCollection->addFieldToFilter('entity_id', ['in' => implode(',', $productIds)]); + $this->appendRewrites->execute($collection->getItems(), $storeIds); + } + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml index d529c6dd3ecc3..fc9eb8529da6f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml @@ -18,7 +18,7 @@ </annotations> <before> <magentoCLI command="config:set {{DisableCreatePermanentRedirect.path}} {{DisableCreatePermanentRedirect.value}}" stepKey="enableCreatePermanentRedirect"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createDefaultCategory"/> @@ -29,7 +29,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> <magentoCLI command="config:set {{EnableCreatePermanentRedirect.path}} {{EnableCreatePermanentRedirect.value}}" stepKey="disableCreatePermanentRedirect"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml index 776b5b9b70f33..783d9fa31c996 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml @@ -31,9 +31,7 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="indexerReindexAfterCreate"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBefore"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php index 30f7608504c23..0ceff2aeff5e5 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/ProductProcessUrlRewriteSavingObserverTest.php @@ -1,4 +1,5 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. @@ -8,11 +9,12 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Observer; use Magento\Catalog\Model\Product; -use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator; +use Magento\CatalogUrlRewrite\Model\Products\AppendUrlRewritesToProducts; use Magento\CatalogUrlRewrite\Observer\ProductProcessUrlRewriteSavingObserver; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event; use Magento\Framework\Event\Observer; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\StoreResolver\GetStoresListByWebsiteIds; use Magento\UrlRewrite\Model\UrlPersistInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -44,29 +46,39 @@ class ProductProcessUrlRewriteSavingObserverTest extends TestCase protected $product; /** - * @var ProductUrlRewriteGenerator|MockObject + * @var ProductProcessUrlRewriteSavingObserver */ - protected $productUrlRewriteGenerator; + protected $model; /** - * @var ObjectManager + * @var AppendUrlRewritesToProducts|MockObject */ - protected $objectManager; + private $appendRewrites; /** - * @var ProductProcessUrlRewriteSavingObserver + * @var ScopeConfigInterface|MockObject */ - protected $model; + private $scopeConfig; /** - * Set up + * @inheritdoc */ protected function setUp(): void { $this->urlPersist = $this->getMockForAbstractClass(UrlPersistInterface::class); $this->product = $this->getMockBuilder(Product::class) ->addMethods(['getIsChangedWebsites', 'getIsChangedCategories']) - ->onlyMethods(['getId', 'dataHasChangedFor', 'isVisibleInSiteVisibility', 'getStoreId']) + ->onlyMethods( + [ + 'getId', + 'dataHasChangedFor', + 'getVisibility', + 'getStoreId', + 'getWebsiteIds', + 'getOrigData', + 'getCategoryCollection', + ] + ) ->disableOriginalConstructor() ->getMock(); $this->product->expects($this->any())->method('getId')->willReturn(3); @@ -77,20 +89,27 @@ protected function setUp(): void $this->event->expects($this->any())->method('getProduct')->willReturn($this->product); $this->observer = $this->createPartialMock(Observer::class, ['getEvent']); $this->observer->expects($this->any())->method('getEvent')->willReturn($this->event); - $this->productUrlRewriteGenerator = $this->createPartialMock( - ProductUrlRewriteGenerator::class, - ['generate'] - ); - $this->productUrlRewriteGenerator->expects($this->any()) - ->method('generate') - ->willReturn([3 => 'rewrite']); - $this->objectManager = new ObjectManager($this); - $this->model = $this->objectManager->getObject( - ProductProcessUrlRewriteSavingObserver::class, - [ - 'productUrlRewriteGenerator' => $this->productUrlRewriteGenerator, - 'urlPersist' => $this->urlPersist - ] + + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->onlyMethods(['isSetFlag']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->appendRewrites = $this->getMockBuilder(AppendUrlRewritesToProducts::class) + ->onlyMethods(['execute']) + ->disableOriginalConstructor() + ->getMock(); + + $getStoresList = $this->getMockBuilder(GetStoresListByWebsiteIds::class) + ->onlyMethods(['execute']) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = new ProductProcessUrlRewriteSavingObserver( + $this->urlPersist, + $this->appendRewrites, + $this->scopeConfig, + $getStoresList ); } @@ -103,53 +122,59 @@ public function urlKeyDataProvider() { return [ 'url changed' => [ - 'isChangedUrlKey' => true, - 'isChangedVisibility' => false, - 'isChangedWebsites' => false, - 'isChangedCategories' => false, - 'visibilityResult' => true, - 'expectedReplaceCount' => 1, + 'isChangedUrlKey' => true, + 'isChangedVisibility' => false, + 'isChangedWebsites' => false, + 'isChangedCategories' => false, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 1, + 'websitesWithProduct' => [1], ], 'no chnages' => [ - 'isChangedUrlKey' => false, - 'isChangedVisibility' => false, - 'isChangedWebsites' => false, - 'isChangedCategories' => false, - 'visibilityResult' => true, - 'expectedReplaceCount' => 0 + 'isChangedUrlKey' => false, + 'isChangedVisibility' => false, + 'isChangedWebsites' => false, + 'isChangedCategories' => false, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 0, + 'websitesWithProduct' => [1], ], 'visibility changed' => [ - 'isChangedUrlKey' => false, - 'isChangedVisibility' => true, - 'isChangedWebsites' => false, - 'isChangedCategories' => false, - 'visibilityResult' => true, - 'expectedReplaceCount' => 1 + 'isChangedUrlKey' => false, + 'isChangedVisibility' => true, + 'isChangedWebsites' => false, + 'isChangedCategories' => false, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 1, + 'websitesWithProduct' => [1], ], 'websites changed' => [ - 'isChangedUrlKey' => false, - 'isChangedVisibility' => false, - 'isChangedWebsites' => true, - 'isChangedCategories' => false, - 'visibilityResult' => true, - 'expectedReplaceCount' => 1 + 'isChangedUrlKey' => false, + 'isChangedVisibility' => false, + 'isChangedWebsites' => true, + 'isChangedCategories' => false, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 1, + 'websitesWithProduct' => [1], ], 'categories changed' => [ - 'isChangedUrlKey' => false, - 'isChangedVisibility' => false, - 'isChangedWebsites' => false, - 'isChangedCategories' => true, - 'visibilityResult' => true, - 'expectedReplaceCount' => 1 + 'isChangedUrlKey' => false, + 'isChangedVisibility' => false, + 'isChangedWebsites' => false, + 'isChangedCategories' => true, + 'visibilityResult' => 4, + 'expectedReplaceCount' => 1, + 'websitesWithProduct' => [1], ], 'url changed invisible' => [ - 'isChangedUrlKey' => true, - 'isChangedVisibility' => false, - 'isChangedWebsites' => false, - 'isChangedCategories' => false, - 'visibilityResult' => false, - 'expectedReplaceCount' => 0 + 'isChangedUrlKey' => true, + 'isChangedVisibility' => false, + 'isChangedWebsites' => false, + 'isChangedCategories' => false, + 'visibilityResult' => 1, + 'expectedReplaceCount' => 0, + 'websitesWithProduct' => [1], ], ]; } @@ -161,6 +186,7 @@ public function urlKeyDataProvider() * @param bool $isChangedCategories * @param bool $visibilityResult * @param int $expectedReplaceCount + * @param array $websitesWithProduct * * @dataProvider urlKeyDataProvider */ @@ -170,16 +196,19 @@ public function testExecuteUrlKey( $isChangedWebsites, $isChangedCategories, $visibilityResult, - $expectedReplaceCount + $expectedReplaceCount, + $websitesWithProduct ) { $this->product->expects($this->any())->method('getStoreId')->willReturn(12); $this->product->expects($this->any()) ->method('dataHasChangedFor') - ->willReturnMap([ - ['visibility', $isChangedVisibility], - ['url_key', $isChangedUrlKey] - ]); + ->willReturnMap( + [ + ['visibility', $isChangedVisibility], + ['url_key', $isChangedUrlKey], + ] + ); $this->product->expects($this->any()) ->method('getIsChangedWebsites') @@ -189,13 +218,23 @@ public function testExecuteUrlKey( ->method('getIsChangedCategories') ->willReturn($isChangedCategories); + $this->product->expects($this->any())->method('getWebsiteIds')->will( + $this->returnValue($websitesWithProduct) + ); + $this->product->expects($this->any()) - ->method('isVisibleInSiteVisibility') + ->method('getVisibility') ->willReturn($visibilityResult); - $this->urlPersist->expects($this->exactly($expectedReplaceCount)) - ->method('replace') - ->with([3 => 'rewrite']); + $this->product->expects($this->any()) + ->method('getOrigData') + ->willReturn($isChangedWebsites ? [] : $websitesWithProduct); + $this->scopeConfig->expects($this->any()) + ->method('isSetFlag') + ->willReturn(true); + + $this->appendRewrites->expects($this->exactly($expectedReplaceCount)) + ->method('execute'); $this->model->execute($this->observer); } diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml index 5fb7d33546d60..d22816243f64c 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml @@ -27,6 +27,9 @@ <type name="Magento\CatalogUrlRewrite\Model\Storage\DbStorage"> <plugin name="dynamic_storage_plugin" type="Magento\CatalogUrlRewrite\Plugin\DynamicCategoryRewrites"/> </type> + <type name="Magento\Catalog\Model\Product\Action"> + <plugin name="update_url_rewrites_after_websites_update_plugin" type="Magento\CatalogUrlRewrite\Plugin\Catalog\Model\Product\UpdateProductWebsiteUrlRewrites"/> + </type> <type name="Magento\CatalogUrlRewrite\Model\Map\UrlRewriteFinder"> <arguments> <argument name="urlRewriteClassNames" xsi:type="array"> diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php index 59708d90c23b7..f1cec1c15d861 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php @@ -55,7 +55,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): string { + ): ?string { /** @var StoreInterface $store */ $store = $context->getExtensionAttributes()->getStore(); $storeId = (int)$store->getId(); @@ -66,16 +66,16 @@ public function resolve( * Retrieve category url suffix by store * * @param int $storeId - * @return string + * @return string|null */ - private function getCategoryUrlSuffix(int $storeId): string + private function getCategoryUrlSuffix(int $storeId): ?string { if (!isset($this->categoryUrlSuffix[$storeId])) { $this->categoryUrlSuffix[$storeId] = $this->scopeConfig->getValue( self::$xml_path_category_url_suffix, ScopeInterface::SCOPE_STORE, $storeId - ); + ) ?? ''; } return $this->categoryUrlSuffix[$storeId]; } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php index 9a0193ba36367..db84784bab5b6 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php @@ -55,7 +55,7 @@ public function resolve( ResolveInfo $info, array $value = null, array $args = null - ): string { + ): ?string { /** @var StoreInterface $store */ $store = $context->getExtensionAttributes()->getStore(); $storeId = (int)$store->getId(); @@ -66,16 +66,16 @@ public function resolve( * Retrieve product url suffix by store * * @param int $storeId - * @return string + * @return string|null */ - private function getProductUrlSuffix(int $storeId): string + private function getProductUrlSuffix(int $storeId): ?string { if (!isset($this->productUrlSuffix[$storeId])) { $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue( self::$xml_path_product_url_suffix, ScopeInterface::SCOPE_STORE, $storeId - ); + ) ?? ''; } return $this->productUrlSuffix[$storeId]; } diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/CategoryUrlSuffixTest.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/CategoryUrlSuffixTest.php new file mode 100644 index 0000000000000..1d8cb6a391064 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/CategoryUrlSuffixTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogUrlRewriteGraphQl\Test\Unit\Model\Resolver; + +use Magento\CatalogUrlRewriteGraphQl\Model\Resolver\CategoryUrlSuffix; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextExtensionInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Store\Api\Data\StoreInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Test for \Magento\CatalogUrlRewriteGraphQl\Model\Resolver\CategoryUrlSuffix. + */ +class CategoryUrlSuffixTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var ContextInterface|MockObject + */ + private $contextMock; + + /** + * @var ContextExtensionInterface|MockObject + */ + private $contextExtensionMock; + + /** + * @var StoreInterface|MockObject + */ + private $storeMock; + + /** + * @var Field|MockObject + */ + private $fieldMock; + + /** + * @var ResolveInfo|MockObject + */ + private $resolveInfoMock; + + /** + * @var CategoryUrlSuffix + */ + private $resolver; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(ContextInterface::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getExtensionAttributes' + ] + ) + ->getMockForAbstractClass(); + + $this->contextExtensionMock = $this->getMockBuilder(ContextExtensionInterface::class) + ->setMethods( + [ + 'getStore' + ] + ) + ->getMockForAbstractClass(); + + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods( + [ + 'getId' + ] + ) + ->getMockForAbstractClass(); + + $this->fieldMock = $this->getMockBuilder(Field::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resolveInfoMock = $this->getMockBuilder(ResolveInfo::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + + $this->resolver = new CategoryUrlSuffix( + $this->scopeConfigMock + ); + } + + /** + * Verify that empty string is returned when config value is null + */ + public function testNullValue() + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn(null); + + $this->contextMock + ->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->contextExtensionMock); + + $this->contextExtensionMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock + ->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + '', + $this->resolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock + ) + ); + } + + /** + * Verify that the configured value is returned + */ + public function testNonNullValue() + { + $value = 'html'; + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn($value); + + $this->contextMock + ->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->contextExtensionMock); + + $this->contextExtensionMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock + ->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + $value, + $this->resolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock + ) + ); + } +} diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/ProductUrlSuffixTest.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/ProductUrlSuffixTest.php new file mode 100644 index 0000000000000..e133ff8dc5855 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Test/Unit/Model/Resolver/ProductUrlSuffixTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\CatalogUrlRewriteGraphQl\Test\Unit\Model\Resolver; + +use Magento\CatalogUrlRewriteGraphQl\Model\Resolver\ProductUrlSuffix; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextExtensionInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Store\Api\Data\StoreInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Test for \Magento\CatalogUrlRewriteGraphQl\Model\Resolver\ProductUrlSuffix. + */ +class ProductUrlSuffixTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var ContextInterface|MockObject + */ + private $contextMock; + + /** + * @var ContextExtensionInterface|MockObject + */ + private $contextExtensionMock; + + /** + * @var StoreInterface|MockObject + */ + private $storeMock; + + /** + * @var Field|MockObject + */ + private $fieldMock; + + /** + * @var ResolveInfo|MockObject + */ + private $resolveInfoMock; + + /** + * @var ProductUrlSuffix + */ + private $resolver; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->contextMock = $this->getMockBuilder(ContextInterface::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'getExtensionAttributes' + ] + ) + ->getMockForAbstractClass(); + + $this->contextExtensionMock = $this->getMockBuilder(ContextExtensionInterface::class) + ->setMethods( + [ + 'getStore' + ] + ) + ->getMockForAbstractClass(); + + $this->storeMock = $this->getMockBuilder(StoreInterface::class) + ->setMethods( + [ + 'getId' + ] + ) + ->getMockForAbstractClass(); + + $this->fieldMock = $this->getMockBuilder(Field::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->resolveInfoMock = $this->getMockBuilder(ResolveInfo::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + + $this->resolver = new ProductUrlSuffix( + $this->scopeConfigMock + ); + } + + /** + * Verify that empty string is returned when config value is null + */ + public function testNullValue() + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn(null); + + $this->contextMock + ->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->contextExtensionMock); + + $this->contextExtensionMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock + ->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + '', + $this->resolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock + ) + ); + } + + /** + * Verify that the configured value is returned + */ + public function testNonNullValue() + { + $value = 'html'; + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn($value); + + $this->contextMock + ->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->contextExtensionMock); + + $this->contextExtensionMock + ->expects($this->once()) + ->method('getStore') + ->willReturn($this->storeMock); + + $this->storeMock + ->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->assertEquals( + $value, + $this->resolver->resolve( + $this->fieldMock, + $this->contextMock, + $this->resolveInfoMock + ) + ); + } +} diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckSelectedShippingAddressInCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckSelectedShippingAddressInCheckoutActionGroup.xml index 0c952af6d53fa..ab588bd49436f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckSelectedShippingAddressInCheckoutActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckSelectedShippingAddressInCheckoutActionGroup.xml @@ -16,13 +16,12 @@ <argument name="customerVar"/> <argument name="customerAddressVar"/> </arguments> - <waitForElement selector="{{CheckoutShippingSection.shippingTab}}" time="30" stepKey="waitForShippingSectionLoaded"/> - <see stepKey="VerifyFirstNameInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.firstname}}"/> - <see stepKey="VerifyLastNameInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.lastname}}"/> - <see stepKey="VerifyStreetInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.street[0]}}"/> - <see stepKey="VerifyCityInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.city}}"/> - <see stepKey="VerifyZipInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.postcode}}"/> - <see stepKey="VerifyPhoneInSelectedAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.telephone}}"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.firstname}}" stepKey="VerifyFirstNameInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerVar.lastname}}" stepKey="VerifyLastNameInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.street[0]}}" stepKey="VerifyStreetInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.city}}" stepKey="VerifyCityInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.postcode}}" stepKey="VerifyZipInSelectedAddress"/> + <see selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{customerAddressVar.telephone}}" stepKey="VerifyPhoneInSelectedAddress"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup.xml new file mode 100644 index 0000000000000..8b1d9665fbdf8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCheckSelectedBillingAddressInCheckoutWithSearchActionGroup"> + <annotations> + <description>Validates that the provided Customer and Address details are listed on the Storefront Checkout page under the 'Billing Address' section when multiple Addresses are present for a Customer.</description> + </annotations> + <arguments> + <argument name="customerVar"/> + <argument name="customerAddressVar"/> + </arguments> + <waitForElement selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" time="30" stepKey="waitForBillingSectionLoaded"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerVar.firstname}}" stepKey="verifyFirstNameInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerVar.lastname}}" stepKey="verifyLastNameInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerAddressVar.street[0]}}" stepKey="verifyStreetInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerAddressVar.city}}" stepKey="verifyCityInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerAddressVar.postcode}}" stepKey="verifyZipInSelectedAddress"/> + <see selector="{{CheckoutBillingAddressSection.selectedBillingAddress}}" userInput="{{customerAddressVar.telephone}}" stepKey="verifyPhoneInSelectedAddress"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickSaveAddressButtonActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickSaveAddressButtonActionGroup.xml index a1e7497aa9d43..3e72f5e78a101 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickSaveAddressButtonActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickSaveAddressButtonActionGroup.xml @@ -12,8 +12,9 @@ <annotations> <description>Click Save Address button on checkout.</description> </annotations> - - <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="saveAddress"/> + <waitForElementVisible selector="{{CheckoutShippingSection.saveAddress}}" stepKey="waitForSaveButton"/> + <click selector="{{CheckoutShippingSection.saveAddress}}" stepKey="clickSaveButton"/> <waitForPageLoad stepKey="waitForAddressSaved"/> + <waitForElementNotVisible selector="{{CheckoutShippingSection.newAddressForm}}" stepKey="waitForNewAddressFormGone"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickAddNewAddressButtonFromCheckoutShippingActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickAddNewAddressButtonFromCheckoutShippingActionGroup.xml new file mode 100644 index 0000000000000..34a120d111d0b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickAddNewAddressButtonFromCheckoutShippingActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickAddNewAddressButtonFromCheckoutShippingActionGroup"> + <annotations> + <description>Clicks the New Address button on the storefront Checkout Shipping page</description> + </annotations> + <waitForElementVisible selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="waitForAddNewAddressButton"/> + <click selector="{{CheckoutShippingSection.newAddressButton}}" stepKey="clickAddNewAddressButton"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + <waitForElementVisible selector="{{CheckoutShippingSection.newAddressForm}}" stepKey="waitForNewAddressForm"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickUpdateAddressInCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickUpdateAddressInCheckoutActionGroup.xml new file mode 100644 index 0000000000000..e83fc452b6962 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClickUpdateAddressInCheckoutActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickUpdateAddressInCheckoutActionGroup"> + <annotations> + <description>Clicks the Update button on the checkout page when entering a New Address.</description> + </annotations> + <waitForElementVisible selector="{{CheckoutShippingSection.updateAddress}}" stepKey="waitForUpdateButton"/> + <click selector="{{CheckoutShippingSection.updateAddress}}" stepKey="clickUpdateButton"/> + <waitForPageLoad stepKey="waitForAddressSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectAddressInCheckoutAddressDropDownActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectAddressInCheckoutAddressDropDownActionGroup.xml new file mode 100644 index 0000000000000..39338d490bc35 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectAddressInCheckoutAddressDropDownActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectAddressInCheckoutAddressDropDownActionGroup"> + <annotations> + <description>Selects the specified option in the address selection drop down on the storefront Checkout page.</description> + </annotations> + <arguments> + <argument name="address" defaultValue="New Address" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutPaymentSection.addressDropdown}}" stepKey="waitForAddressDropDownToBeVisible"/> + <selectOption selector="{{CheckoutPaymentSection.addressDropdown}}" userInput="{{address}}" stepKey="selectAddressOption"/> + <waitForPageLoad stepKey="waitForAddressLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup.xml new file mode 100644 index 0000000000000..574ede3fe54d1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectCustomerAddressOnPaymentStepInCheckoutActionGroup"> + <annotations> + <description>Selects the specified address after 'Change Address' pop up has been opened on the Storefront Checkout page on the 'Payment' step.</description> + </annotations> + <arguments> + <argument name="address" type="string"/> + </arguments> + <waitForElementVisible selector="{{CheckoutBillingAddressSearchSection.selectButton(address)}}" stepKey="waitForAddress"/> + <click selector="{{CheckoutBillingAddressSearchSection.selectButton(address)}}" stepKey="clickSelectForAddress"/> + <waitForElementNotVisible selector="{{CheckoutShippingAddressSearchSection.popupSelectShippingAddress}}" stepKey="waitForPopupClosed"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml index 59d46e8cca696..a5eba8835e354 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml @@ -15,6 +15,7 @@ <element name="editAddressButton" type="button" selector=".action-edit-address" timeout="30"/> <element name="addressDropdown" type="select" selector="[name=billing_address_id]"/> <element name="newAddressButton" type="button" selector=".action-show-popup" timeout="30"/> + <element name="newAddressForm" type="text" selector="#co-shipping-form"/> <element name="email" type="input" selector="input[id*=customer-email]"/> <element name="password" type="input" selector="#customer-password"/> <element name="firstName" type="input" selector="input[name=firstname]"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index df229c4b6ed78..ffbd6152af80b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -58,6 +58,11 @@ <argument name="address" value="US_Address_TX"/> </actionGroup> + <!-- Select Free Shipping --> + <actionGroup ref="StorefrontSetShippingMethodActionGroup" stepKey="setShippingMethodFreeShipping"> + <argument name="shippingMethodName" value="Free Shipping"/> + </actionGroup> + <!-- Assert Free Shipping checkbox --> <seeCheckboxIsChecked selector="{{CheckoutShippingMethodsSection.shippingMethodFreeShipping}}" stepKey="freeShippingMethodCheckboxIsChecked"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml index 1b9831e7dd0bd..d46ebfd63203a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml @@ -31,11 +31,12 @@ </actionGroup> </before> <after> + <!--Logout from customer account--> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <!--Logout from customer account--> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <comment userInput="BIC workaround" stepKey="logoutCustomer"/> </after> <!-- Add simple product to cart and go to checkout--> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml index ce8bfc37389fb..38e2203b45258 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml @@ -107,13 +107,14 @@ <deleteData createDataKey="createDownloadableProduct" stepKey="deleteDownloadableProduct"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> - - <!-- Delete customer --> - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <comment userInput="BIC workaround" stepKey="deleteCustomer"/> <!-- Logout customer --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCreatedCustomer"/> + <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml index f31e8342ead23..45f1df1237d09 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -7,9 +7,9 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest"> + <test name="StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest" deprecated="Use StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest instead"> <annotations> - <title value="Checkout Free Shipping Recalculation after Coupon Code Added"/> + <title value="DEPRECATED. Checkout Free Shipping Recalculation after Coupon Code Added"/> <stories value="Checkout Free Shipping Recalculation after Coupon Code Added"/> <description value="User should be able to do checkout free shipping recalculation after adding coupon code"/> <features value="Checkout"/> @@ -17,8 +17,10 @@ <testCaseId value="MAGETWO-96537"/> <useCaseId value="MAGETWO-96431"/> <group value="Checkout"/> + <skip> + <issueId value="DEPRECATED">Use StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest instead</issueId> + </skip> </annotations> - <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="Simple_US_Customer" stepKey="createSimpleUsCustomer"> @@ -61,7 +63,6 @@ </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartRule"> <argument name="product" value="$$simpleProduct$$"/> <argument name="couponCode" value="{{CatPriceRule.coupon_code}}"/> @@ -87,9 +88,8 @@ <waitForPageLoad stepKey="waitForPageLoad"/> <see userInput="Your coupon was successfully applied." stepKey="seeSuccessMessage"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> - <!-- Cannot use waitForPageLoad as the below error message will disappear after a few seconds & waitForPageLoad will cause this test to be flaky --> - <comment userInput="BIC workaround" stepKey="waitForError"/> - <waitForText stepKey="seeShippingMethodError" userInput="The shipping method is missing. Select the shipping method and try again."/> + <waitForPageLoad stepKey="waitForError"/> + <seeElementInDOM selector="{{CheckoutHeaderSection.errorMessageContainsText('The shipping method is missing. Select the shipping method and try again.')}}" stepKey="seeShippingMethodError"/> <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> <waitForPageLoad stepKey="waitForShippingPageLoad"/> <click stepKey="chooseFlatRateShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Flat Rate')}}"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml new file mode 100644 index 0000000000000..bc9dddf53ad03 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout Free Shipping Recalculation after Coupon Code Added"/> + <title value="Checkout Free Shipping Recalculation after Coupon Code Added"/> + <description value="User should be able to do checkout free shipping recalculation after adding coupon code"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-28548"/> + <useCaseId value="MAGETWO-96431"/> + <group value="Checkout"/> + </annotations> + + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"> + <field key="group_id">1</field> + </createData> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="_defaultProduct" stepKey="simpleProduct"> + <field key="price">90</field> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + <!--It is default for FlatRate--> + <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <createData entity="MinimumOrderAmount90" stepKey="minimumOrderAmount90"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllCartPriceRules"/> + <actionGroup ref="AdminCreateCartPriceRuleWithCouponCodeActionGroup" stepKey="createCartPriceRule"> + <argument name="ruleName" value="CatPriceRule"/> + <argument name="couponCode" value="CatPriceRule.coupon_code"/> + </actionGroup> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStoreFront"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$simpleProduct.custom_attributes[url_key]$"/> + </actionGroup> + </before> + + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllCartPriceRules"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="ApplyCartRuleOnStorefrontActionGroup" stepKey="applyCartRule"> + <argument name="product" value="$simpleProduct$"/> + <argument name="couponCode" value="{{CatPriceRule.coupon_code}}"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart1"/> + <waitForPageLoad stepKey="waitForpageLoad1"/> + <dontSee selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}" stepKey="dontSeeFreeShipping"/> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage"/> + <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="openDiscountTabIfClosed"/> + <waitForPageLoad stepKey="waitForCouponTabOpen1"/> + <click selector="{{DiscountSection.CancelCoupon}}" stepKey="cancelCoupon"/> + <waitForPageLoad stepKey="waitForCancel"/> + <see userInput='You canceled the coupon code.' stepKey="seeCancellationMessage"/> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart2"/> + <waitForPageLoad stepKey="waitForShippingMethods"/> + <click selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Free')}}" stepKey="chooseFreeShipping"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextAfterFreeShippingMethodSelection"/> + <waitForPageLoad stepKey="waitForReviewAndPayments"/> + <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyCouponCode"> + <argument name="discountCode" value="{{CatPriceRule.coupon_code}}"/> + </actionGroup> + <!-- Assert order cannot be placed and error message will shown. --> + <actionGroup ref="AssertStorefrontOrderIsNotPlacedActionGroup" stepKey="seeShippingMethodError"> + <argument name="error" value="The shipping method is missing. Select the shipping method and try again."/> + </actionGroup> + <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> + <waitForPageLoad stepKey="waitForShippingPageLoad"/> + + <click stepKey="chooseFlatRateShipping" selector="{{CheckoutShippingMethodsSection.shippingMethodRowByName('Flat Rate')}}"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextAfterFlatRateShippingMethodSelection"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <!-- Place Order --> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml index 3c090900563a5..714a06510952f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml @@ -94,7 +94,7 @@ <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="selectViewAndEditCart"/> <!--Assert Shopping Cart Summary--> - <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="AssertCartSummary" > <argument name="subtotal" value="$100.00"/> <argument name="shipping" value="10.00"/> <argument name="total" value="110.00"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml index eff18f9081b67..cd6f4215adb5d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml @@ -88,7 +88,7 @@ <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="selectViewAndEditCart"/> <!--Assert Shopping Cart Summary--> - <actionGroup ref="AssertStorefrontShoppingCartSummaryWithShippingActionGroup" stepKey="AssertCartSummary" > + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="AssertCartSummary" > <argument name="subtotal" value="$50.00"/> <argument name="shipping" value="5.00"/> <argument name="total" value="55.00"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutForShowShippingMethodNoApplicableTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutForShowShippingMethodNoApplicableTest.xml index 22bc1260e5f33..3c06ca17cfedc 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutForShowShippingMethodNoApplicableTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutForShowShippingMethodNoApplicableTest.xml @@ -58,6 +58,6 @@ <click selector="{{CheckoutShippingMethodsSection.next}}" stepKey="clickNextButton"/> <!-- Assert order cannot be placed and error message will shown. --> <waitForPageLoad stepKey="waitForError"/> - <see stepKey="seeShippingMethodError" userInput="The shipping method is missing. Select the shipping method and try again."/> + <seeElementInDOM selector="{{CheckoutHeaderSection.errorMessageContainsText('The shipping method is missing. Select the shipping method and try again')}}" stepKey="seeShippingMethodError"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml index e97f7f0d3e8e4..535a3fb0c436b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml @@ -28,12 +28,13 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteSimpleCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteUsCustomer"/> <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="resetCustomerFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <comment userInput="BIC workaround" stepKey="logoutCustomer"/> </after> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="storefrontCustomerLogin"> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index a59ea7101f16c..d296999c88b53 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -27,7 +27,8 @@ define([ // clone address form data to new object var addressData = $.extend(true, {}, formData), region, - regionName = addressData.region; + regionName = addressData.region, + customAttributes; if (mageUtils.isObject(addressData.street)) { addressData.street = this.objectToArray(addressData.street); @@ -64,10 +65,20 @@ define([ addressData['custom_attributes'] = _.map( addressData['custom_attributes'], function (value, key) { - return { + customAttributes = { 'attribute_code': key, 'value': value }; + + if (typeof value === 'boolean') { + customAttributes = { + 'attribute_code': key, + 'value': value, + 'label': value === true ? 'Yes' : 'No' + }; + } + + return customAttributes; } ); } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js index fd12eed76ed50..71e6c39b4e319 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/cart/estimate-service.js @@ -80,5 +80,4 @@ define([ quote.shippingAddress.subscribe(estimateTotalsAndUpdateRates); quote.shippingMethod.subscribe(estimateTotalsShipping); quote.billingAddress.subscribe(estimateTotalsBilling); - customerData.get('cart').subscribe(estimateTotalsShipping); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html index 23bbce48fee2c..6b3de69d1a21d 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/billing-address/details.html @@ -7,7 +7,7 @@ <div if="isAddressDetailsVisible() && currentBillingAddress()" class="billing-address-details"> <text args="currentBillingAddress().prefix"/> <text args="currentBillingAddress().firstname"/> <text args="currentBillingAddress().middlename"/> <text args="currentBillingAddress().lastname"/> <text args="currentBillingAddress().suffix"/><br/> - <text args="_.values(currentBillingAddress().street).join(', ')"/><br/> + <text args="currentBillingAddress().street.join(', ')"/><br/> <text args="currentBillingAddress().city "/>, <span text="currentBillingAddress().region"></span> <text args="currentBillingAddress().postcode"/><br/> <text args="getCountryName(currentBillingAddress().countryId)"/><br/> <a if="currentBillingAddress().telephone" attr="'href': 'tel:' + currentBillingAddress().telephone" text="currentBillingAddress().telephone"></a><br/> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/CliEnableTinyMCE4ActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CliEnableTinyMCE4ActionGroup.xml new file mode 100644 index 0000000000000..9e49762f3de20 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/CliEnableTinyMCE4ActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliEnableTinyMCE4ActionGroup"> + <annotations> + <description>Enable Tiny MCE 4 by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{EnableTinyMCE4.path}} {{EnableTinyMCE4.value}}" stepKey="enableTinyMCE4"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index c0424e09f8f76..e2111aac348cb 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -22,7 +22,7 @@ <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <actionGroup ref="AssignBlockToCMSPage" stepKey="assignBlockToCMSPage"> <argument name="Block" value="$$createPreReqBlock$$"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml index 4f67b81446ae7..39f44a3270944 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml @@ -21,7 +21,7 @@ <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <after> <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> @@ -33,7 +33,7 @@ <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> <argument name="CMSPage" value="$$createCMSPage$$"/> </actionGroup> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGBlockTest.xml index 32bd75d373115..963844710dd7f 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGBlockTest.xml @@ -22,7 +22,7 @@ <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Create Custom Variable--> <actionGroup ref="CreateCustomVariableActionGroup" stepKey="createCustomVariable" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml index 698f29a28598f..7f3d2537a9b0d 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml @@ -19,7 +19,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Create Custom Variable--> <actionGroup ref="CreateCustomVariableActionGroup" stepKey="createCustomVariable" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGBlockTest.xml index 887fe88533f74..9e28e81c2696d 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGBlockTest.xml @@ -22,7 +22,7 @@ <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage"> <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml index 509e1abe81ef6..a599d22eab298 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml @@ -21,7 +21,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml index cfb323683dc2c..1c9d7b38a40a4 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml @@ -22,7 +22,7 @@ <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml index d9ea67491e30a..4f78f6bcce5e0 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml @@ -21,7 +21,7 @@ <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> <actionGroup ref="ConfigAdminAccountSharingActionGroup" stepKey="allowAdminShareAccount"/> </before> <!--Main test--> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml index 86f90e0e2a580..4ee5b0d263d40 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml @@ -25,7 +25,7 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml index dcb4c3dc11f3c..6c36913cbb593 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml @@ -27,7 +27,7 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml index 6acf8ef18a332..440f63403c519 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml @@ -26,7 +26,7 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml index 1ec4f7054e8c2..226292e6cdea4 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml @@ -25,7 +25,7 @@ </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml index c1d5f39d2a005..c5ab9d13b848a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCheckCreateFolderEscapeAndEnterHandlesForWYSIWYGBlockTest.xml @@ -23,7 +23,7 @@ <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <after> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckOrderOfProdsInWidgetOnCMSPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckOrderOfProdsInWidgetOnCMSPageTest.xml index 484dc16faa3b9..c0e6a9cbd793d 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/CheckOrderOfProdsInWidgetOnCMSPageTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckOrderOfProdsInWidgetOnCMSPageTest.xml @@ -22,7 +22,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="enableTinyMCE4"/> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="enableTinyMCE4"/> <waitForPageLoad stepKey="waitConfigToSave"/> <createData entity="ApiCategory" stepKey="createFirstCategory"/> <createData entity="ApiSimpleProduct" stepKey="product1"> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest.xml index 58f3b9d5bd3b1..03e3097dbd720 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnBlockTest.xml @@ -22,7 +22,7 @@ <createData entity="_defaultCmsPage" stepKey="createPreReqCMSPage" /> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <amOnPage url="{{CmsNewBlock.url}}" stepKey="amOnNewBlockPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest.xml index 43615ac906b00..82e725de46249 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCMSPageTest.xml @@ -21,7 +21,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnPagePagesGrid"/> <waitForPageLoad stepKey="waitForPageLoad1"/> diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/AddProductsToCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/AddProductsToCompareList.php new file mode 100644 index 0000000000000..2aab3a0cb266e --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/AddProductsToCompareList.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Resolver; + +use Magento\Catalog\Model\MaskedListIdToCompareListId; +use Magento\CompareListGraphQl\Model\Service\AddToCompareList; +use Magento\CompareListGraphQl\Model\Service\Customer\GetListIdByCustomerId; +use Magento\CompareListGraphQl\Model\Service\GetCompareList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Add products item to compare list + */ +class AddProductsToCompareList implements ResolverInterface +{ + /** + * @var AddToCompareList + */ + private $addProductToCompareList; + + /** + * @var GetCompareList + */ + private $getCompareList; + + /** + * @var MaskedListIdToCompareListId + */ + private $maskedListIdToCompareListId; + + /** + * @var GetListIdByCustomerId + */ + private $getListIdByCustomerId; + + /** + * @param AddToCompareList $addProductToCompareList + * @param GetCompareList $getCompareList + * @param MaskedListIdToCompareListId $maskedListIdToCompareListId + * @param GetListIdByCustomerId $getListIdByCustomerId + */ + public function __construct( + AddToCompareList $addProductToCompareList, + GetCompareList $getCompareList, + MaskedListIdToCompareListId $maskedListIdToCompareListId, + GetListIdByCustomerId $getListIdByCustomerId + ) { + $this->addProductToCompareList = $addProductToCompareList; + $this->getCompareList = $getCompareList; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + $this->getListIdByCustomerId = $getListIdByCustomerId; + } + + /** + * Add products to compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($args['input']['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified.')); + } + + if (!isset($args['input']['products'])) { + throw new GraphQlInputException(__('"products" value must be specified.')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['input']['uid'], $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + + if (!$listId) { + throw new GraphQlInputException(__('"uid" value does not exist')); + } + + try { + $this->addProductToCompareList->execute($listId, $args['input']['products'], $context); + } catch (\Exception $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + return $this->getCompareList->execute($listId, $context); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/AssignCompareListToCustomer.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/AssignCompareListToCustomer.php new file mode 100644 index 0000000000000..8c43fcf5e9299 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/AssignCompareListToCustomer.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Resolver; + +use Magento\Catalog\Model\MaskedListIdToCompareListId; +use Magento\CompareListGraphQl\Model\Service\Customer\SetCustomerToCompareList; +use Magento\CompareListGraphQl\Model\Service\GetCompareList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Class Assign Customer to CompareList + */ +class AssignCompareListToCustomer implements ResolverInterface +{ + /** + * @var SetCustomerToCompareList + */ + private $setCustomerToCompareList; + + /** + * @var MaskedListIdToCompareListId + */ + private $maskedListIdToCompareListId; + + /** + * @var GetCompareList + */ + private $getCompareList; + + /** + * @param SetCustomerToCompareList $setCustomerToCompareList + * @param MaskedListIdToCompareListId $maskedListIdToCompareListId + */ + public function __construct( + SetCustomerToCompareList $setCustomerToCompareList, + MaskedListIdToCompareListId $maskedListIdToCompareListId, + GetCompareList $getCompareList + ) { + $this->setCustomerToCompareList = $setCustomerToCompareList; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + $this->getCompareList = $getCompareList; + } + + /** + * Assign compare list to customer + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($args['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified')); + } + + if (!$context->getUserId()) { + throw new GraphQlInputException(__('Customer must be logged')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['uid']); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + if ($listId) { + try { + $result = $this->setCustomerToCompareList->execute($listId, $context->getUserId(), $context); + if ($result) { + return [ + 'result' => true, + 'compare_list' => $this->getCompareList->execute((int)$result->getListId(), $context) + ]; + } + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during assigning customer.') + ); + } + } + + return [ + 'result' => false + ]; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/CompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/CompareList.php new file mode 100644 index 0000000000000..861f3ce36d555 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/CompareList.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Resolver; + +use Magento\Catalog\Model\MaskedListIdToCompareListId; +use Magento\CompareListGraphQl\Model\Service\GetCompareList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Get compare list + */ +class CompareList implements ResolverInterface +{ + /** + * @var GetCompareList + */ + private $getCompareList; + + /** + * @var MaskedListIdToCompareListId + */ + private $maskedListIdToCompareListId; + + /** + * @param GetCompareList $getCompareList + * @param MaskedListIdToCompareListId $maskedListIdToCompareListId + */ + public function __construct( + GetCompareList $getCompareList, + MaskedListIdToCompareListId $maskedListIdToCompareListId + ) { + $this->getCompareList = $getCompareList; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + } + + /** + * Get compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($args['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['uid'], $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + if (!$listId) { + return null; + } + + return $this->getCompareList->execute($listId, $context); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/CreateCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/CreateCompareList.php new file mode 100644 index 0000000000000..9b0e8fd18298f --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/CreateCompareList.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Resolver; + +use Magento\CompareListGraphQl\Model\Service\AddToCompareList; +use Magento\CompareListGraphQl\Model\Service\CreateCompareList as CreateCompareListService; +use Magento\CompareListGraphQl\Model\Service\Customer\GetListIdByCustomerId; +use Magento\CompareListGraphQl\Model\Service\GetCompareList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Math\Random; + +/** + * Class for creating compare list + */ +class CreateCompareList implements ResolverInterface +{ + /** + * @var Random + */ + private $mathRandom; + + /** + * @var GetListIdByCustomerId + */ + private $getListIdByCustomerId; + + /** + * @var AddToCompareList + */ + private $addProductToCompareList; + + /** + * @var GetCompareList + */ + private $getCompareList; + + /** + * @var CreateCompareListService + */ + private $createCompareList; + + /** + * @param Random $mathRandom + * @param GetListIdByCustomerId $getListIdByCustomerId + * @param AddToCompareList $addProductToCompareList + * @param GetCompareList $getCompareList + * @param CreateCompareListService $createCompareList + */ + public function __construct( + Random $mathRandom, + GetListIdByCustomerId $getListIdByCustomerId, + AddToCompareList $addProductToCompareList, + GetCompareList $getCompareList, + CreateCompareListService $createCompareList + ) { + $this->mathRandom = $mathRandom; + $this->getListIdByCustomerId = $getListIdByCustomerId; + $this->addProductToCompareList = $addProductToCompareList; + $this->getCompareList = $getCompareList; + $this->createCompareList = $createCompareList; + } + + /** + * Create compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws GraphQlInputException + * @throws LocalizedException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $customerId = $context->getUserId(); + $products = !empty($args['input']['products']) ? $args['input']['products'] : []; + $generatedListId = $this->mathRandom->getUniqueHash(); + $listId = 0; + + try { + if ((0 === $customerId || null === $customerId)) { + $listId = $this->createCompareList->execute($generatedListId); + $this->addProductToCompareList->execute($listId, $products, $context); + } + + if ($customerId) { + $listId = $this->getListIdByCustomerId->execute($customerId); + if ($listId) { + $this->addProductToCompareList->execute($listId, $products, $context); + } else { + $listId = $this->createCompareList->execute($generatedListId, $customerId); + $this->addProductToCompareList->execute($listId, $products, $context); + } + } + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during creating compare list') + ); + } + + return $this->getCompareList->execute($listId, $context); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/CustomerCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/CustomerCompareList.php new file mode 100644 index 0000000000000..84d0aad0c9a71 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/CustomerCompareList.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Resolver; + +use Magento\CompareListGraphQl\Model\Service\Customer\GetListIdByCustomerId; +use Magento\CompareListGraphQl\Model\Service\GetCompareList; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Get customer compare list + */ +class CustomerCompareList implements ResolverInterface +{ + /** + * @var GetCompareList + */ + private $getCompareList; + + /** + * @var GetListIdByCustomerId + */ + private $getListIdByCustomerId; + + /** + * @param GetCompareList $getCompareList + * @param GetListIdByCustomerId $getListIdByCustomerId + */ + public function __construct( + GetCompareList $getCompareList, + GetListIdByCustomerId $getListIdByCustomerId + ) { + $this->getCompareList = $getCompareList; + $this->getListIdByCustomerId = $getListIdByCustomerId; + } + + /** + * Get customer compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $listId = $this->getListIdByCustomerId->execute((int)$context->getUserId()); + + if (!$listId) { + return null; + } + + return $this->getCompareList->execute($listId, $context); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/DeleteCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/DeleteCompareList.php new file mode 100644 index 0000000000000..a811478718985 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/DeleteCompareList.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Resolver; + +use Magento\Catalog\Model\CompareListFactory; +use Magento\Catalog\Model\MaskedListIdToCompareListId; +use Magento\Catalog\Model\ResourceModel\Product\Compare\CompareList as CompareListResource; +use Magento\CompareListGraphQl\Model\Service\Customer\GetListIdByCustomerId; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Class for deleting compare list + */ +class DeleteCompareList implements ResolverInterface +{ + /** + * @var CompareListFactory + */ + private $compareListFactory; + + /** + * @var CompareListResource + */ + private $compareListResource; + + /** + * @var MaskedListIdToCompareListId + */ + private $maskedListIdToCompareListId; + + /** + * @var GetListIdByCustomerId + */ + private $getListIdByCustomerId; + + /** + * @param CompareListFactory $compareListFactory + * @param CompareListResource $compareListResource + * @param MaskedListIdToCompareListId $maskedListIdToCompareListId + * @param GetListIdByCustomerId $getListIdByCustomerId + */ + public function __construct( + CompareListFactory $compareListFactory, + CompareListResource $compareListResource, + MaskedListIdToCompareListId $maskedListIdToCompareListId, + GetListIdByCustomerId $getListIdByCustomerId + ) { + $this->compareListFactory = $compareListFactory; + $this->compareListResource = $compareListResource; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + $this->getListIdByCustomerId = $getListIdByCustomerId; + } + + /** + * Remove compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (empty($args['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['uid'], $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + $removed = ['result' => false]; + + if ($userId = $context->getUserId()) { + $customerListId = $this->getListIdByCustomerId->execute($userId); + if ($listId === $customerListId) { + try { + $removed['result'] = $this->deleteCompareList($customerListId); + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during removing compare list') + ); + } + } + } + + if ($listId) { + try { + $removed['result'] = $this->deleteCompareList($listId); + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during removing compare list') + ); + } + } + + return $removed; + } + + /** + * Delete compare list + * + * @param int|null $listId + * @return bool + */ + private function deleteCompareList(?int $listId): bool + { + $compareList = $this->compareListFactory->create(); + $compareList->setListId($listId); + $this->compareListResource->delete($compareList); + + return true; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Resolver/RemoveProductsFromCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Resolver/RemoveProductsFromCompareList.php new file mode 100644 index 0000000000000..6e4e4d8951cb9 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Resolver/RemoveProductsFromCompareList.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Resolver; + +use Magento\Catalog\Model\MaskedListIdToCompareListId; +use Magento\CompareListGraphQl\Model\Service\Customer\GetListIdByCustomerId; +use Magento\CompareListGraphQl\Model\Service\GetCompareList; +use Magento\CompareListGraphQl\Model\Service\RemoveFromCompareList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Remove items from compare list + */ +class RemoveProductsFromCompareList implements ResolverInterface +{ + /** + * @var GetCompareList + */ + private $getCompareList; + + /** + * @var RemoveFromCompareList + */ + private $removeFromCompareList; + + /** + * @var MaskedListIdToCompareListId + */ + private $maskedListIdToCompareListId; + + /** + * @var GetListIdByCustomerId + */ + private $getListIdByCustomerId; + + /** + * @param GetCompareList $getCompareList + * @param RemoveFromCompareList $removeFromCompareList + * @param MaskedListIdToCompareListId $maskedListIdToCompareListId + * @param GetListIdByCustomerId $getListIdByCustomerId + */ + public function __construct( + GetCompareList $getCompareList, + RemoveFromCompareList $removeFromCompareList, + MaskedListIdToCompareListId $maskedListIdToCompareListId, + GetListIdByCustomerId $getListIdByCustomerId + ) { + $this->getCompareList = $getCompareList; + $this->removeFromCompareList = $removeFromCompareList; + $this->maskedListIdToCompareListId = $maskedListIdToCompareListId; + $this->getListIdByCustomerId = $getListIdByCustomerId; + } + + /** + * Remove products from compare list + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return Value|mixed|void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @throws GraphQlInputException + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($args['input']['products'])) { + throw new GraphQlInputException(__('"products" value must be specified.')); + } + + if (empty($args['input']['uid'])) { + throw new GraphQlInputException(__('"uid" value must be specified.')); + } + + try { + $listId = $this->maskedListIdToCompareListId->execute($args['input']['uid'], $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + + if (!$listId) { + throw new GraphQlInputException(__('"uid" value does not exist')); + } + + if ($userId = $context->getUserId()) { + $customerListId = $this->getListIdByCustomerId->execute($userId); + if ($listId === $customerListId) { + $this->removeFromCompareList($customerListId, $args); + } + } + + try { + $this->removeFromCompareList->execute($listId, $args['input']['products']); + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during removing products from compare list') + ); + } + + return $this->getCompareList->execute($listId, $context); + } + + /** + * Remove products from compare list + * + * @param int $listId + * @param array $args + * @throws GraphQlInputException + */ + private function removeFromCompareList(int $listId, array $args): void + { + try { + $this->removeFromCompareList->execute($listId, $args['input']['products']); + } catch (LocalizedException $exception) { + throw new GraphQlInputException( + __('Something was wrong during removing products from compare list') + ); + } + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/AddToCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/AddToCompareList.php new file mode 100644 index 0000000000000..6f976bdd07d4d --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/AddToCompareList.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service; + +use Magento\Catalog\Model\Product\Compare\ItemFactory; +use Magento\Catalog\Model\ProductRepository; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * Service add product to compare list + */ +class AddToCompareList +{ + /** + * @var ItemFactory + */ + private $compareItemFactory; + + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @var Collection + */ + private $itemCollection; + + /** + * @param ItemFactory $compareItemFactory + * @param ProductRepository $productRepository + * @param Collection $collection + */ + public function __construct( + ItemFactory $compareItemFactory, + ProductRepository $productRepository, + Collection $collection + ) { + $this->compareItemFactory = $compareItemFactory; + $this->productRepository = $productRepository; + $this->itemCollection = $collection; + } + + /** + * Add products to compare list + * + * @param int $listId + * @param array $products + * @param ContextInterface $context + * + * @return int + * @throws \Exception + */ + public function execute(int $listId, array $products, ContextInterface $context): int + { + $storeId = (int)$context->getExtensionAttributes()->getStore()->getStoreId(); + $customerId = $context->getUserId(); + if ($customerId) { + $this->itemCollection->setListIdToCustomerCompareItems($listId, $customerId); + } + + if (count($products)) { + $existedProducts = $this->itemCollection->getProductsByListId($listId); + foreach ($products as $productId) { + if (array_search($productId, $existedProducts) === false) { + if ($this->productExists($productId)) { + $item = $this->compareItemFactory->create(); + if ($customerId) { + $item->setCustomerId($customerId); + } + $item->addProductData($productId); + $item->setStoreId($storeId); + $item->setListId($listId); + $item->save(); + } + } + } + } + + return (int)$listId; + } + + /** + * Check product exists. + * + * @param int $productId + * + * @return bool + */ + private function productExists($productId) + { + try { + $product = $this->productRepository->getById((int)$productId); + return !empty($product->getId()); + } catch (NoSuchEntityException $e) { + return false; + } + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/Collection/GetComparableItemsCollection.php b/app/code/Magento/CompareListGraphQl/Model/Service/Collection/GetComparableItemsCollection.php new file mode 100644 index 0000000000000..e766ec85248a1 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/Collection/GetComparableItemsCollection.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service\Collection; + +use Magento\Catalog\Helper\Product\Compare; +use Magento\Catalog\Model\Config as CatalogConfig; +use Magento\Catalog\Model\Product\Visibility as CatalogProductVisibility; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\CollectionFactory as CompareItemsCollectionFactory; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * Get collection with comparable items + */ +class GetComparableItemsCollection +{ + /** + * @var Collection + */ + private $items; + + /** + * @var CompareItemsCollectionFactory + */ + private $itemCollectionFactory; + + /** + * @var CatalogProductVisibility + */ + private $catalogProductVisibility; + + /** + * @var CatalogConfig + */ + private $catalogConfig; + + /** + * @var Compare + */ + private $compareProduct; + + /** + * @param CompareItemsCollectionFactory $itemCollectionFactory + * @param CatalogProductVisibility $catalogProductVisibility + * @param CatalogConfig $catalogConfig + * @param Compare $compareHelper + */ + public function __construct( + CompareItemsCollectionFactory $itemCollectionFactory, + CatalogProductVisibility $catalogProductVisibility, + CatalogConfig $catalogConfig, + Compare $compareHelper + ) { + $this->itemCollectionFactory = $itemCollectionFactory; + $this->catalogProductVisibility = $catalogProductVisibility; + $this->catalogConfig = $catalogConfig; + $this->compareProduct = $compareHelper; + } + + /** + * Get collection of comparable items + * + * @param int $listId + * @param ContextInterface $context + * + * @return Collection + */ + public function execute(int $listId, ContextInterface $context): Collection + { + $this->compareProduct->setAllowUsedFlat(false); + $this->items = $this->itemCollectionFactory->create(); + $this->items->setListId($listId); + $this->items->useProductItem()->setStoreId($context->getExtensionAttributes()->getStore()->getStoreId()); + $this->items->addAttributeToSelect( + $this->catalogConfig->getProductAttributes() + )->loadComparableAttributes()->addMinimalPrice()->addTaxPercents()->setVisibility( + $this->catalogProductVisibility->getVisibleInSiteIds() + ); + + return $this->items; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/CreateCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/CreateCompareList.php new file mode 100644 index 0000000000000..089bdb1adef17 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/CreateCompareList.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service; + +use Magento\Catalog\Model\CompareListFactory; +use Magento\Catalog\Model\ResourceModel\Product\Compare\CompareList as CompareListResource; + +/** + * Create new Compare List + */ +class CreateCompareList +{ + /** + * @var CompareListFactory + */ + private $compareListFactory; + + /** + * @var CompareListResource + */ + private $compareListResource; + + /** + * @param CompareListFactory $compareListFactory + * @param CompareListResource $compareListResource + */ + public function __construct( + CompareListFactory $compareListFactory, + CompareListResource $compareListResource + ) { + $this->compareListFactory = $compareListFactory; + $this->compareListResource = $compareListResource; + } + + /** + * Created new compare list + * + * @param string $maskedId + * @param int $customerId + * + * @return int + */ + public function execute(string $maskedId, ?int $customerId = null): int + { + $compareList = $this->compareListFactory->create(); + $compareList->setListIdMask($maskedId); + $compareList->setCustomerId($customerId); + $this->compareListResource->save($compareList); + + return (int)$compareList->getListId(); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/Customer/GetListIdByCustomerId.php b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/GetListIdByCustomerId.php new file mode 100644 index 0000000000000..c6437683f6ba7 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/GetListIdByCustomerId.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service\Customer; + +use Magento\Catalog\Model\CompareList; +use Magento\Catalog\Model\CompareListFactory; +use Magento\Catalog\Model\ResourceModel\Product\Compare\CompareList as ResourceCompareList; + +/** + * Get compare list id by customer id + */ +class GetListIdByCustomerId +{ + /** + * @var CompareListFactory + */ + private $compareListFactory; + + /** + * @var ResourceCompareList + */ + private $resourceCompareList; + + /** + * @param CompareListFactory $compareListFactory + * @param ResourceCompareList $resourceCompareList + */ + public function __construct( + CompareListFactory $compareListFactory, + ResourceCompareList $resourceCompareList + ) { + $this->compareListFactory = $compareListFactory; + $this->resourceCompareList = $resourceCompareList; + } + + /** + * Get listId by Customer ID + * + * @param int $customerId + * + * @return int|null + */ + public function execute(int $customerId): ?int + { + if ($customerId) { + /** @var CompareList $compareList */ + $compareList = $this->compareListFactory->create(); + $this->resourceCompareList->load($compareList, $customerId, 'customer_id'); + return (int)$compareList->getListId(); + } + + return null; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/Customer/SetCustomerToCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/SetCustomerToCompareList.php new file mode 100644 index 0000000000000..72216c6c70a16 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/SetCustomerToCompareList.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service\Customer; + +use Magento\Catalog\Model\CompareList; +use Magento\Catalog\Model\CompareListFactory; +use Magento\Catalog\Model\ResourceModel\Product\Compare\CompareList as ResourceCompareList; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\Collection; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item\CollectionFactory as CompareItemsCollectionFactory; +use Magento\CompareListGraphQl\Model\Service\AddToCompareList; +use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * Assign customer to compare list + */ +class SetCustomerToCompareList +{ + /** + * @var ValidateCustomer + */ + private $validateCustomer; + + /** + * @var CompareListFactory + */ + private $compareListFactory; + + /** + * @var ResourceCompareList + */ + private $resourceCompareList; + + /** + * @var GetListIdByCustomerId + */ + private $getListIdByCustomerId; + + /** + * @var Collection + */ + private $items; + + /** + * @var CompareItemsCollectionFactory + */ + private $itemCollectionFactory; + + /** + * @var AddToCompareList + */ + private $addProductToCompareList; + + /** + * @param ValidateCustomer $validateCustomer + * @param CompareListFactory $compareListFactory + * @param ResourceCompareList $resourceCompareList + */ + public function __construct( + ValidateCustomer $validateCustomer, + CompareListFactory $compareListFactory, + ResourceCompareList $resourceCompareList, + GetListIdByCustomerId $getListIdByCustomerId, + CompareItemsCollectionFactory $itemCollectionFactory, + AddToCompareList $addProductToCompareList + ) { + $this->validateCustomer = $validateCustomer; + $this->compareListFactory = $compareListFactory; + $this->resourceCompareList = $resourceCompareList; + $this->getListIdByCustomerId = $getListIdByCustomerId; + $this->itemCollectionFactory = $itemCollectionFactory; + $this->addProductToCompareList = $addProductToCompareList; + } + + /** + * Set customer to compare list + * + * @param int $listId + * @param int $customerId + * + * @return CompareList + * + * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute(int $listId, int $customerId, ContextInterface $context): ?CompareList + { + if ($this->validateCustomer->execute($customerId)) { + /** @var CompareList $compareListModel */ + $compareList = $this->compareListFactory->create(); + $customerListId = $this->getListIdByCustomerId->execute($customerId); + $this->resourceCompareList->load($compareList, $listId, 'list_id'); + if ($customerListId) { + $this->items = $this->itemCollectionFactory->create(); + $products = $this->items->getProductsByListId($listId); + $this->addProductToCompareList->execute($customerListId, $products, $context); + $this->resourceCompareList->delete($compareList); + $compareList = $this->compareListFactory->create(); + $this->resourceCompareList->load($compareList, $customerListId, 'list_id'); + return $compareList; + } + $compareList->setCustomerId($customerId); + $this->resourceCompareList->save($compareList); + return $compareList; + } + + return null; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/Customer/ValidateCustomer.php b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/ValidateCustomer.php new file mode 100644 index 0000000000000..ab16b17240b1c --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/Customer/ValidateCustomer.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service\Customer; + +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\AuthenticationInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; + +/** + * Class provided customer validation + */ +class ValidateCustomer +{ + /** + * @var AuthenticationInterface + */ + private $authentication; + + /** + * @var AccountManagementInterface + */ + private $accountManagement; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @param AuthenticationInterface $authentication + * @param AccountManagementInterface $accountManagement + * @param CustomerRepositoryInterface $customerRepository + */ + public function __construct( + AuthenticationInterface $authentication, + AccountManagementInterface $accountManagement, + CustomerRepositoryInterface $customerRepository + ) { + $this->authentication = $authentication; + $this->accountManagement = $accountManagement; + $this->customerRepository = $customerRepository; + } + + /** + * Customer validate + * + * @param int $customerId + * + * @return int + * + * @throws GraphQlAuthenticationException + * @throws GraphQlInputException + * @throws GraphQlNoSuchEntityException + */ + public function execute(int $customerId): int + { + try { + $customer = $this->customerRepository->getById($customerId); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException( + __('Customer with id "%customer_id" does not exist.', ['customer_id' => $customerId]), + $e + ); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if (true === $this->authentication->isLocked($customerId)) { + throw new GraphQlAuthenticationException(__('The account is locked.')); + } + + try { + $confirmationStatus = $this->accountManagement->getConfirmationStatus($customerId); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + if ($confirmationStatus === AccountManagementInterface::ACCOUNT_CONFIRMATION_REQUIRED) { + throw new GraphQlAuthenticationException(__("This account isn't confirmed. Verify and try again.")); + } + + return (int)$customer->getId(); + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableAttributes.php b/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableAttributes.php new file mode 100644 index 0000000000000..5b051a3825aac --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableAttributes.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service; + +use Magento\CompareListGraphQl\Model\Service\Collection\GetComparableItemsCollection as ComparableItemsCollection; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * Get products comparable attributes + */ +class GetComparableAttributes +{ + /** + * @var ComparableItemsCollection + */ + private $comparableItemsCollection; + + /** + * @param ComparableItemsCollection $comparableItemsCollection + */ + public function __construct( + ComparableItemsCollection $comparableItemsCollection + ) { + $this->comparableItemsCollection = $comparableItemsCollection; + } + + /** + * Get comparable attributes + * + * @param int $listId + * @param ContextInterface $context + * + * @return array + */ + public function execute(int $listId, ContextInterface $context): array + { + $attributes = []; + $itemsCollection = $this->comparableItemsCollection->execute($listId, $context); + foreach ($itemsCollection->getComparableAttributes() as $item) { + $attributes[] = [ + 'code' => $item->getAttributeCode(), + 'label' => $item->getStoreLabel() + ]; + } + + return $attributes; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableItems.php b/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableItems.php new file mode 100644 index 0000000000000..1cf42553718fd --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/GetComparableItems.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service; + +use Magento\Catalog\Block\Product\Compare\ListCompare; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ProductRepository; +use Magento\CompareListGraphQl\Model\Service\Collection\GetComparableItemsCollection as ComparableItemsCollection; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * Get comparable products + */ +class GetComparableItems +{ + /** + * @var ListCompare + */ + private $blockListCompare; + + /** + * @var ComparableItemsCollection + */ + private $comparableItemsCollection; + + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @param ListCompare $listCompare + * @param ComparableItemsCollection $comparableItemsCollection + * @param ProductRepository $productRepository + */ + public function __construct( + ListCompare $listCompare, + ComparableItemsCollection $comparableItemsCollection, + ProductRepository $productRepository + ) { + $this->blockListCompare = $listCompare; + $this->comparableItemsCollection = $comparableItemsCollection; + $this->productRepository = $productRepository; + } + + /** + * Get comparable items + * + * @param int $listId + * @param ContextInterface $context + * + * @return array + * @throws GraphQlInputException + */ + public function execute(int $listId, ContextInterface $context) + { + $items = []; + foreach ($this->comparableItemsCollection->execute($listId, $context) as $item) { + /** @var Product $item */ + $items[] = [ + 'uid' => $item->getId(), + 'product' => $this->getProductData((int)$item->getId()), + 'attributes' => $this->getProductComparableAttributes($listId, $item, $context) + ]; + } + + return $items; + } + + /** + * Get product data + * + * @param int $productId + * + * @return array + * + * @throws GraphQlInputException + */ + private function getProductData(int $productId): array + { + $productData = []; + try { + $item = $this->productRepository->getById($productId); + $productData = $item->getData(); + $productData['model'] = $item; + } catch (LocalizedException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + return $productData; + } + + /** + * Get comparable attributes for product + * + * @param int $listId + * @param Product $product + * @param ContextInterface $context + * + * @return array + */ + private function getProductComparableAttributes(int $listId, Product $product, ContextInterface $context): array + { + $attributes = []; + $itemsCollection = $this->comparableItemsCollection->execute($listId, $context); + foreach ($itemsCollection->getComparableAttributes() as $item) { + $attributes[] = [ + 'code' => $item->getAttributeCode(), + 'value' => $this->blockListCompare->getProductAttributeValue($product, $item) + ]; + } + + return $attributes; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/GetCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/GetCompareList.php new file mode 100644 index 0000000000000..5cef555479838 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/GetCompareList.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service; + +use Magento\Catalog\Model\CompareListIdToMaskedListId; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; + +/** + * Get products compare list + */ +class GetCompareList +{ + /** + * @var GetComparableItems + */ + private $comparableItemsService; + + /** + * @var GetComparableAttributes + */ + private $comparableAttributesService; + + /** + * @var CompareListIdToMaskedListId + */ + private $compareListIdToMaskedListId; + + /** + * @param GetComparableItems $comparableItemsService + * @param GetComparableAttributes $comparableAttributesService + * @param CompareListIdToMaskedListId $compareListIdToMaskedListId + */ + public function __construct( + GetComparableItems $comparableItemsService, + GetComparableAttributes $comparableAttributesService, + CompareListIdToMaskedListId $compareListIdToMaskedListId + ) { + $this->comparableItemsService = $comparableItemsService; + $this->comparableAttributesService = $comparableAttributesService; + $this->compareListIdToMaskedListId = $compareListIdToMaskedListId; + } + + /** + * Get compare list information + * + * @param int $listId + * @param ContextInterface $context + * + * @return array + * @throws GraphQlInputException + */ + public function execute(int $listId, ContextInterface $context) + { + try { + $maskedListId = $this->compareListIdToMaskedListId->execute($listId, $context->getUserId()); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__($exception->getMessage())); + } + $comparableItems = $this->comparableItemsService->execute($listId, $context); + + return [ + 'uid' => $maskedListId, + 'items' => $comparableItems, + 'attributes' => $this->comparableAttributesService->execute($listId, $context), + 'item_count' => count($comparableItems) + ]; + } +} diff --git a/app/code/Magento/CompareListGraphQl/Model/Service/RemoveFromCompareList.php b/app/code/Magento/CompareListGraphQl/Model/Service/RemoveFromCompareList.php new file mode 100644 index 0000000000000..dda400559a412 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/Model/Service/RemoveFromCompareList.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CompareListGraphQl\Model\Service; + +use Magento\Catalog\Model\Product\Compare\Item; +use Magento\Catalog\Model\ResourceModel\Product\Compare\Item as CompareItemResource; +use Magento\Catalog\Model\Product\Compare\ItemFactory; + +/** + * Remove product from compare list + */ +class RemoveFromCompareList +{ + /** + * @var ItemFactory + */ + private $compareItemFactory; + + /** + * @var CompareItemResource + */ + private $compareItemResource; + + /** + * @param ItemFactory $compareItemFactory + * @param CompareItemResource $compareItemResource + */ + public function __construct( + ItemFactory $compareItemFactory, + CompareItemResource $compareItemResource + ) { + $this->compareItemFactory = $compareItemFactory; + $this->compareItemResource = $compareItemResource; + } + + /** + * Remove products from compare list + * + * @param int $listId + * @param array $products + */ + public function execute(int $listId, array $products) + { + foreach ($products as $productId) { + /* @var $item Item */ + $item = $this->compareItemFactory->create(); + $item->setListId($listId); + $this->compareItemResource->loadByProduct($item, $productId); + if ($item->getId()) { + $this->compareItemResource->delete($item); + } + } + } +} diff --git a/app/code/Magento/CompareListGraphQl/README.md b/app/code/Magento/CompareListGraphQl/README.md new file mode 100644 index 0000000000000..ed1c38ab33a3b --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/README.md @@ -0,0 +1,4 @@ +# CompareListGraphQl module + +The CompareListGraphQl module is designed to implement compare product functionality. + diff --git a/app/code/Magento/CompareListGraphQl/composer.json b/app/code/Magento/CompareListGraphQl/composer.json new file mode 100644 index 0000000000000..dd9c998857258 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-compare-list-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\CompareListGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/CompareListGraphQl/etc/module.xml b/app/code/Magento/CompareListGraphQl/etc/module.xml new file mode 100644 index 0000000000000..b3c330fc38df2 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/etc/module.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_CompareListGraphQl"> + <sequence> + <module name="Magento_Catalog"/> + <module name="Magento_Customer"/> + <module name="Magento_GraphQl"/> + <module name="Magento_CatalogGraphQl"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/CompareListGraphQl/etc/schema.graphqls b/app/code/Magento/CompareListGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..e533d476ddd59 --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/etc/schema.graphqls @@ -0,0 +1,64 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type ComparableItem { + uid: ID! @doc(description: "The unique ID of an item in a compare list") + product: ProductInterface! @doc(description: "Contains details about a product in a compare list") + attributes: [ProductAttribute]! @doc(description: "An array of product attributes that can be used to compare products") +} + +type ProductAttribute { + code: String! @doc(description: "The unique identifier for a product attribute code.") + value: String! @doc(description:"The display value of the attribute") +} + +type ComparableAttribute { + code: String! @doc(description: "An attribute code that is enabled for product comparisons") + label: String! @doc(description: "The label of the attribute code") +} + +type CompareList { + uid: ID! @doc(description: "The unique ID assigned to the compare list") + items: [ComparableItem] @doc(description: "An array of products to compare") + attributes: [ComparableAttribute] @doc(description: "An array of attributes that can be used for comparing products") + item_count: Int! @doc(description: "The number of items in the compare list") +} + +type Customer { + compare_list: CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\CustomerCompareList") @doc(description: "The contents of the customer's compare list") +} + +type Query { + compareList(uid: ID!): CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\CompareList") @doc(description: "Return products that have been added to the specified compare list") +} + +type Mutation { + createCompareList(input: CreateCompareListInput): CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\CreateCompareList") @doc(description: "Creates a new compare list. The compare list is saved for logged in customers") + addProductsToCompareList(input: AddProductsToCompareListInput): CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\AddProductsToCompareList") @doc(description: "Add products to the specified compare list") + removeProductsFromCompareList(input: RemoveProductsFromCompareListInput): CompareList @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\RemoveProductsFromCompareList") @doc(description: "Remove products from the specified compare list") + assignCompareListToCustomer(uid: ID!): AssignCompareListToCustomerOutput @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\AssignCompareListToCustomer") @doc(description: "Assign the specified compare list to the logged in customer") + deleteCompareList(uid: ID!): DeleteCompareListOutput @resolver(class: "\\Magento\\CompareListGraphQl\\Model\\Resolver\\DeleteCompareList") @doc(description: "Delete the specified compare list") +} + +input CreateCompareListInput { + products: [ID!] @doc(description: "An array of product IDs to add to the compare list") +} + +input AddProductsToCompareListInput { + uid: ID!, @doc(description: "The unique identifier of the compare list to modify") + products: [ID!]! @doc(description: "An array of product IDs to add to the compare list") +} + +input RemoveProductsFromCompareListInput { + uid: ID!, @doc(description: "The unique identifier of the compare list to modify") + products: [ID!]! @doc(description: "An array of product IDs to remove from the compare list") +} + +type DeleteCompareListOutput { + result: Boolean! @doc(description: "Indicates whether the compare list was successfully deleted") +} + +type AssignCompareListToCustomerOutput { + result: Boolean! + compare_list: CompareList @doc(description: "The contents of the customer's compare list") +} diff --git a/app/code/Magento/CompareListGraphQl/registration.php b/app/code/Magento/CompareListGraphQl/registration.php new file mode 100644 index 0000000000000..bb764b439273d --- /dev/null +++ b/app/code/Magento/CompareListGraphQl/registration.php @@ -0,0 +1,14 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_CompareListGraphQl', + __DIR__ +); diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitcherActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitcherActionGroup.xml index f4f97b14682f3..c6b2605c73d8c 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitcherActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitcherActionGroup.xml @@ -10,9 +10,9 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SwitchToVersion4ActionGroup"> <annotations> - <description>Goes to the 'Configuration' page for 'Content Management'. Sets the 'WYSIWYG Editor' to 'TinyMCE 4'. Clicks on the Save button. PLEASE NOTE: The value is Hardcoded.</description> + <description>DEPRECATED. Use CliEnableTinyMCE4 instead. Goes to the 'Configuration' page for 'Content Management'. Sets the 'WYSIWYG Editor' to 'TinyMCE 4'. Clicks on the Save button. PLEASE NOTE: The value is Hardcoded.</description> </annotations> - + <amOnPage url="{{ConfigurationStoresPage.url}}" stepKey="navigateToWYSIWYGConfigPage1"/> <waitForPageLoad stepKey="waitForConfigPageToLoad"/> <conditionalClick stepKey="expandWYSIWYGOptions" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true"/> diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php index 39fcdf86fdcf4..29c4812cc7b96 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Indexer/Stock/Configurable.php @@ -101,7 +101,7 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f $select->columns(['status' => $stockStatusExpr]); if ($entityIds !== null) { - $select->where('e.entity_id IN(?)', $entityIds); + $select->where('e.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } return $select; diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php index 9d779d9704c29..0dd38062e5a65 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php @@ -203,7 +203,7 @@ public function getParentIdsByChild($childId) ['e' => $this->getTable('catalog_product_entity')], 'e.' . $this->optionProvider->getProductEntityLinkField() . ' = l.parent_id', ['e.entity_id'] - )->where('l.product_id IN(?)', $childId); + )->where('l.product_id IN(?)', $childId, \Zend_Db::INT_TYPE); $parentIds = $this->getConnection()->fetchCol($select); return $parentIds; diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml new file mode 100644 index 0000000000000..0b9b5c98d9884 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckProductQtyAfterOrderCancellingTest.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCheckProductQtyAfterOrderCancellingTest"> + + <annotations> + <features value="ConfigurableProduct"/> + <stories value="Cancel order"/> + <title value="Product quantity return after order cancel"/> + <description value="Check Product quantity return after order cancel"/> + <severity value="CRITICAL"/> + <testCaseId value="MAGETWO-97228"/> + <useCaseId value="MAGETWO-82221"/> + <group value="ConfigurableProduct"/> + </annotations> + + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + + <createData entity="defaultSimpleProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="GuestCart" stepKey="createGuestCart"/> + <createData entity="FourCartItems" stepKey="addCartItem"> + <requiredEntity createDataKey="createGuestCart"/> + <requiredEntity createDataKey="createConfigProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddress"> + <requiredEntity createDataKey="createGuestCart"/> + </createData> + <updateData createDataKey="createGuestCart" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformation"> + <requiredEntity createDataKey="createGuestCart"/> + </updateData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + </after> + + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> + <argument name="orderId" value="$createGuestCart.return$"/> + </actionGroup> + + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="openOrder"/> + + <actionGroup ref="AdminInvoiceWithUpdatedProductQtyActionGroup" stepKey="createPartialInvoice"> + <argument name="qty" value="1"/> + </actionGroup> + + <actionGroup ref="AdminCreateShipmentFromOrderPage" stepKey="createShipment"> + <argument name="Qty" value="1"/> + <argument name="Number" value="111"/> + </actionGroup> + + <actionGroup ref="CancelPendingOrderActionGroup" stepKey="cancelOrder"> + <argument name="orderStatus" value="Complete"/> + </actionGroup> + + <see selector="{{AdminOrderItemsOrderedSection.itemQty('1')}}" userInput="Canceled 3" stepKey="seeCanceledQty"/> + + <actionGroup ref="AdminOpenCatalogProductPageActionGroup" stepKey="goToCatalogProductPage"/> + + <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySku"> + <argument name="sku" value="$$createConfigProduct.sku$$"/> + </actionGroup> + + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="assertProductDataInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Quantity"/> + <argument name="value" value="99"/> + </actionGroup> + + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + + </test> +</tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml index f8cd1760788a8..4915a17c738a4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -91,9 +91,7 @@ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="seeConfigurationsPrice"/> <!--Run re-index task--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Assert storefront category list page--> <amOnPage url="/" stepKey="amOnStorefront"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml index 120734d679d09..4984d296df5d0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -131,10 +131,7 @@ <!-- Save product --> <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveProduct"/> - <!--Run re-index task--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!-- Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index e34bf7c22f06b..1b8dffbd3ac68 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -78,9 +78,7 @@ <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexAll"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml index 6e977a7749145..e0dae94f13150 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml @@ -121,7 +121,11 @@ <argument name="productId" value="$$createConfigChildProduct2.id$$"/> </actionGroup> <waitForPageLoad stepKey="waitForSecondChildProductPageLoad"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="outOfStockStatus"/> + + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="outOfStockStatus"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSecondProductForm"/> <!-- Go to created customer page --> <comment userInput="Go to created customer page" stepKey="goToCreatedCustomerPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml index 0b7bca201ec32..4baceead08a07 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml @@ -8,17 +8,20 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ProductsQtyReturnAfterOrderCancelTest"> + <test name="ProductsQtyReturnAfterOrderCancelTest" deprecated="Use AdminCheckProductQtyAfterOrderCancellingTest instead"> <annotations> <features value="ConfigurableProduct"/> <stories value="Cancel order"/> - <title value="Product quantity return after order cancel"/> + <title value="DEPRECATED. Product quantity return after order cancel"/> <description value="Check Product quantity return after order cancel"/> <severity value="CRITICAL"/> <testCaseId value="MAGETWO-97228"/> <useCaseId value="MAGETWO-82221"/> <group value="ConfigurableProduct"/> + <skip> + <issueId value="DEPRECATED">Use AdminCheckProductQtyAfterOrderCancellingTest instead</issueId> + </skip> </annotations> <before> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml index 4ad2d0dc936eb..ca3065c13ea67 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -144,9 +144,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexAll"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml index e20a6dcfa09b8..81a083c5da068 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml @@ -27,9 +27,7 @@ <argument name="category" value="$$createCategory$$"/> </actionGroup> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value="eav"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml index 3519503c1e287..65ba89d5efb1f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml @@ -112,9 +112,7 @@ <argument name="categoryName" value="$$secondCategory.name$$"/> </actionGroup> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexSearchIndex"> - <argument name="indices" value="catalogsearch_fulltext"/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexSearchIndex"/> <!-- Go to storefront to view child product --> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToSecondCategoryStorefront"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml index 363a8ea4d4fd6..9cbe134116e51 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -153,9 +153,7 @@ <argument name="discountAmount" value="{{CatalogRuleByPercentWith96Amount.discount_amount}}"/> </actionGroup> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexIndices"> - <argument name="indices" value="catalogsearch_fulltext catalog_category_product catalog_product_price catalogrule_rule"/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindexIndices"/> <actionGroup ref="CliCacheCleanActionGroup" stepKey="fullCache"> <argument name="tags" value="full_page"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml index 9b046d5c71cfc..ea309271abace 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -132,14 +132,13 @@ <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="selectFirstRow"/> <waitForPageLoad stepKey="waitForProductPageToLoad"/> <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="disableProduct"/> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="disableProduct"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> - <!--Run re-index task--> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <!--Open Category in Store Front and select product attribute option from sidebar --> <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 79705e679fb78..14ce1fe71281b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -120,7 +120,9 @@ <actionGroup ref="AdminFormSaveAndDuplicateActionGroup" stepKey="saveAndDuplicateProductForm"/> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickEnableProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createConfigProduct.name$$-Updated" stepKey="fillProductName"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="1" stepKey="selectInStock"/> + <actionGroup ref="AdminSetStockStatusActionGroup" stepKey="selectInStock"> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> <!--Change product image--> <comment userInput="Change product image" stepKey="commentChangeProductImage"/> <actionGroup ref="RemoveProductImageActionGroup" stepKey="removeProductImage"/> @@ -192,9 +194,7 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateConfigsForDuplicatedProduct"/> <waitForPageLoad stepKey="waitForDuplicatedProductPageLoad"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveDuplicatedProduct"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <comment userInput="Adding the comment to replace CliIndexerReindexActionGroup action group ('indexer:reindex' commands) for preserving Backward Compatibility" stepKey="reindex"/> <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php index 5e3666407a383..19706e114d1d4 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php @@ -14,13 +14,20 @@ use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection as AttributeCollection; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\GraphQl\Query\Uid; /** * Collection for fetching options for all configurable options pulled back in result set. */ class Collection { + /** + * Option type name + */ + private const OPTION_TYPE = 'configurable'; + /** * @var CollectionFactory */ @@ -46,19 +53,26 @@ class Collection */ private $attributeMap = []; + /** @var Uid */ + private $uidEncoder; + /** * @param CollectionFactory $attributeCollectionFactory * @param ProductFactory $productFactory * @param MetadataPool $metadataPool + * @param Uid|null $uidEncoder */ public function __construct( CollectionFactory $attributeCollectionFactory, ProductFactory $productFactory, - MetadataPool $metadataPool + MetadataPool $metadataPool, + Uid $uidEncoder = null ) { $this->attributeCollectionFactory = $attributeCollectionFactory; $this->productFactory = $productFactory; $this->metadataPool = $metadataPool; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -66,7 +80,7 @@ public function __construct( * * @param int $productId */ - public function addProductId(int $productId) : void + public function addProductId(int $productId): void { if (!in_array($productId, $this->productIds)) { $this->productIds[] = $productId; @@ -79,7 +93,7 @@ public function addProductId(int $productId) : void * @param int $productId * @return array */ - public function getAttributesByProductId(int $productId) : array + public function getAttributesByProductId(int $productId): array { $attributes = $this->fetch(); @@ -95,7 +109,7 @@ public function getAttributesByProductId(int $productId) : array * * @return array */ - private function fetch() : array + private function fetch(): array { if (empty($this->productIds) || !empty($this->attributeMap)) { return $this->attributeMap; @@ -121,11 +135,24 @@ private function fetch() : array $attributeData = $attribute->getData(); $this->attributeMap[$productId][$attribute->getId()] = $attribute->getData(); $this->attributeMap[$productId][$attribute->getId()]['id'] = $attribute->getId(); - $this->attributeMap[$productId][$attribute->getId()]['attribute_id_v2'] - = $attribute->getProductAttribute()->getAttributeId(); - $this->attributeMap[$productId][$attribute->getId()]['attribute_code'] - = $attribute->getProductAttribute()->getAttributeCode(); - $this->attributeMap[$productId][$attribute->getId()]['values'] = $attributeData['options']; + $this->attributeMap[$productId][$attribute->getId()]['uid'] = $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $productId . '/' . $attribute->getAttributeId() + ); + $this->attributeMap[$productId][$attribute->getId()]['attribute_id_v2'] = + $attribute->getProductAttribute()->getAttributeId(); + $this->attributeMap[$productId][$attribute->getId()]['attribute_uid'] = + $this->uidEncoder->encode((string) $attribute->getProductAttribute()->getAttributeId()); + $this->attributeMap[$productId][$attribute->getId()]['product_uid'] = + $this->uidEncoder->encode((string) $attribute->getProductId()); + $this->attributeMap[$productId][$attribute->getId()]['attribute_code'] = + $attribute->getProductAttribute()->getAttributeCode(); + $this->attributeMap[$productId][$attribute->getId()]['values'] = array_map( + function ($value) use ($attribute) { + $value['attribute_id'] = $attribute->getAttributeId(); + return $value; + }, + $attributeData['options'] + ); $this->attributeMap[$productId][$attribute->getId()]['label'] = $attribute->getProductAttribute()->getStoreLabel(); } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php index 6624a2624f1c3..92f441f61249c 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php @@ -7,9 +7,12 @@ namespace Magento\ConfigurableProductGraphQl\Model\Resolver; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Helper\Product\Configuration; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Quote\Model\Quote\Item; @@ -19,18 +22,37 @@ */ class ConfigurableCartItemOptions implements ResolverInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'configurable'; + /** * @var Configuration */ private $configurationHelper; + /** @var Uid */ + private $uidEncoder; + + /** + * @var MetadataPool + */ + private $metadataPool; + /** * @param Configuration $configurationHelper + * @param MetadataPool $metadataPool + * @param Uid $uidEncoder */ public function __construct( - Configuration $configurationHelper + Configuration $configurationHelper, + MetadataPool $metadataPool, + Uid $uidEncoder ) { $this->configurationHelper = $configurationHelper; + $this->metadataPool = $metadataPool; + $this->uidEncoder = $uidEncoder; } /** @@ -52,7 +74,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } /** @var Item $cartItem */ $cartItem = $value['model']; - + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $productLinkId = $cartItem->getProduct()->getData($linkField); $result = []; foreach ($this->configurationHelper->getOptions($cartItem) as $option) { if (isset($option['option_type'])) { @@ -61,8 +84,14 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } $result[] = [ 'id' => $option['option_id'], + 'configurable_product_option_uid' => $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $productLinkId . '/' . $option['option_id'] + ), 'option_label' => $option['label'], 'value_id' => $option['option_value'], + 'configurable_product_option_value_uid' => $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $option['option_id'] . '/' . $option['option_value'] + ), 'value_label' => $option['value'], ]; } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php index 13f31e7e2ce10..31cbe58d670b6 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** @@ -23,6 +24,17 @@ class ConfigurableAttributeUid implements ResolverInterface */ private const OPTION_TYPE = 'configurable'; + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + /** * Create a option uid for super attribute in "<option-type>/<attribute-id>/<value-index>" format * @@ -61,7 +73,6 @@ public function resolve( $content = implode('/', $optionDetails); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - return base64_encode($content); + return $this->uidEncoder->encode($content); } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index dc672b02e2f96..227817b3887ba 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -51,4 +51,12 @@ <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder_GraphQl" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> </type> + + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="configurable_thumbnail_source" xsi:type="string">checkout/cart/configurable_product_image</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 6fd3132aa6645..fc177557906ee 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -19,23 +19,26 @@ type ConfigurableAttributeOption @doc(description: "ConfigurableAttributeOption label: String @doc(description: "A string that describes the configurable attribute option") code: String @doc(description: "The ID assigned to the attribute") value_index: Int @doc(description: "A unique index number assigned to the configurable product option") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "The unique ID for a `ConfigurableAttributeOption` object") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") } type ConfigurableProductOptions @doc(description: "ConfigurableProductOptions defines configurable attributes for the specified product") { - id: Int @doc(description: "The configurable option ID number assigned by the system") - attribute_id: String @deprecated(reason: "Use attribute_id_v2 instead") @doc(description: "The ID assigned to the attribute") - attribute_id_v2: Int @doc(description: "The ID assigned to the attribute") + id: Int @deprecated(reason: "Use uid instead") @doc(description: "The configurable option ID number assigned by the system") + uid: ID! @doc(description: "The unique ID for a `ConfigurableProductOptions` object") + attribute_id: String @deprecated(reason: "Use attribute_uid instead") @doc(description: "The ID assigned to the attribute") + attribute_id_v2: Int @deprecated(reason: "Use attribute_uid instead") @doc(description: "The ID assigned to the attribute") + attribute_uid: ID! @doc(description: "The unique ID for a `Attribute` object") attribute_code: String @doc(description: "A string that identifies the attribute") label: String @doc(description: "A string that describes the configurable product option, which is displayed on the UI") position: Int @doc(description: "A number that indicates the order in which the attribute is displayed") use_default: Boolean @doc(description: "Indicates whether the option is the default") values: [ConfigurableProductOptionsValues] @doc(description: "An array that defines the value_index codes assigned to the configurable product") - product_id: Int @doc(description: "This is the same as a product's id field") + product_id: Int @deprecated(reason: "`product_id` is not needed and can be obtained from it's parent") @doc(description: "This is the same as a product's id field") } type ConfigurableProductOptionsValues @doc(description: "ConfigurableProductOptionsValues contains the index number assigned to a configurable product option") { - value_index: Int @doc(description: "A unique index number assigned to the configurable product option") + value_index: Int @deprecated(reason: "Use `uid` instead") @doc(description: "A unique index number assigned to the configurable product option") + uid: ID @doc(description: "The unique ID for a `ConfigurableProductOptionsValues` object") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") label: String @doc(description: "The label of the product") default_label: String @doc(description: "The label of the product on the default store") store_label: String @doc(description: "The label of the product on the current store") @@ -53,7 +56,7 @@ type AddConfigurableProductsToCartOutput { input ConfigurableProductCartItemInput { data: CartItemInput! - variant_sku: String @deprecated(reason: "Use CartItemInput.sku instead.") + variant_sku: String @doc(description: "Deprecated. Use CartItemInput.sku instead.") parent_sku: String @doc(description: "Configurable product SKU.") customizable_options:[CustomizableOptionInput!] } @@ -64,9 +67,11 @@ type ConfigurableCartItem implements CartItemInterface { } type SelectedConfigurableOption { - id: Int! + id: Int! @deprecated(reason: "Use SelectedConfigurableOption.configurable_product_option_uid instead") + configurable_product_option_uid: ID! @doc(description: "The unique ID for a `ConfigurableProductOptions` object") option_label: String! - value_id: Int! + value_id: Int! @deprecated(reason: "Use SelectedConfigurableOption.configurable_product_option_value_uid instead") + configurable_product_option_value_uid: ID! @doc(description: "The unique ID for a `ConfigurableProductOptionsValues` object") value_label: String! } @@ -85,3 +90,7 @@ type ConfigurableOptionAvailableForSelection @doc(description: "Configurable opt option_value_uids: [ID!]! @doc(description: "Configurable option values available for further selection.") attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") } + +type StoreConfig @doc(description: "The type contains information about a store config") { + configurable_thumbnail_source : String @doc(description: "The configuration setting determines which thumbnail should be used in the cart for configurable products.") +} diff --git a/app/code/Magento/Cron/Model/DeadlockRetrier.php b/app/code/Magento/Cron/Model/DeadlockRetrier.php index 15497910a089b..ab180e93e0ca3 100644 --- a/app/code/Magento/Cron/Model/DeadlockRetrier.php +++ b/app/code/Magento/Cron/Model/DeadlockRetrier.php @@ -17,6 +17,20 @@ */ class DeadlockRetrier implements DeadlockRetrierInterface { + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + /** + * @param \Psr\Log\LoggerInterface $logger + */ + public function __construct( + \Psr\Log\LoggerInterface $logger + ) { + $this->logger = $logger; + } + /** * @inheritdoc */ @@ -30,6 +44,7 @@ public function execute(callable $callback, AdapterInterface $connection) try { return $callback(); } catch (DeadlockException $e) { + $this->logger->warning(sprintf("Deadlock detected in cron: %s", $e->getMessage())); continue; } } diff --git a/app/code/Magento/Cron/Model/ResourceModel/Schedule.php b/app/code/Magento/Cron/Model/ResourceModel/Schedule.php index 25ebaec5582c9..120e0ce6432c5 100644 --- a/app/code/Magento/Cron/Model/ResourceModel/Schedule.php +++ b/app/code/Magento/Cron/Model/ResourceModel/Schedule.php @@ -65,31 +65,47 @@ public function trySetJobStatusAtomic($scheduleId, $newStatus, $currentStatus) public function trySetJobUniqueStatusAtomic($scheduleId, $newStatus, $currentStatus) { $connection = $this->getConnection(); + $connection->beginTransaction(); // this condition added to avoid cron jobs locking after incorrect termination of running job $match = $connection->quoteInto( 'existing.job_code = current.job_code ' . - 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL) ' . - 'AND existing.status = ?', + 'AND existing.status = ? ' . + 'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL)', $newStatus ); + // Select and lock all related schedules - this prevents deadlock in case cron overlaps and two jobs of + // the same code attempt to lock at the same time, and force them to serialize $selectIfUnlocked = $connection->select() + ->from( + ['current' => $this->getTable('cron_schedule')], + [] + ) ->joinLeft( ['existing' => $this->getTable('cron_schedule')], $match, - ['status' => new \Zend_Db_Expr($connection->quote($newStatus))] + ['existing.schedule_id'] ) ->where('current.schedule_id = ?', $scheduleId) ->where('current.status = ?', $currentStatus) - ->where('existing.schedule_id IS NULL'); - - $update = $connection->updateFromSelect($selectIfUnlocked, ['current' => $this->getTable('cron_schedule')]); - $result = $connection->query($update)->rowCount(); + ->forUpdate(true); - if ($result == 1) { - return true; + $scheduleId = $connection->fetchOne($selectIfUnlocked); + if (!empty($scheduleId)) { + // Existing running schedule found + $connection->commit(); + return false; } - return false; + + // Mark our schedule as running + $connection->update( + $this->getTable('cron_schedule'), + ['status' => new \Zend_Db_Expr($connection->quote($newStatus))], + ['schedule_id = ?' => $scheduleId] + ); + + $connection->commit(); + return true; } } diff --git a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php index acffba02eb461..0f266b5d62d83 100644 --- a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php +++ b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php @@ -9,6 +9,7 @@ */ namespace Magento\Cron\Observer; +use Magento\Cron\Model\ResourceModel\Schedule\Collection as ScheduleCollection; use Magento\Cron\Model\Schedule; use Magento\Framework\App\State; use Magento\Framework\Console\Cli; @@ -83,7 +84,7 @@ class ProcessCronQueueObserver implements ObserverInterface const MAX_RETRIES = 5; /** - * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection + * @var ScheduleCollection */ protected $_pendingSchedules; @@ -278,12 +279,12 @@ function ($groupId) use ($currentTime) { * * It should be taken by standalone (child) process, not by the parent process. * - * @param int $groupId + * @param string $groupId * @param callable $callback * * @return void */ - private function lockGroup($groupId, callable $callback) + private function lockGroup(string $groupId, callable $callback): void { if (!$this->lockManager->lock(self::LOCK_PREFIX . $groupId, self::LOCK_TIMEOUT)) { $this->logger->warning( @@ -399,7 +400,7 @@ function () use ($schedule) { * @param string $jobName * @return void */ - private function startProfiling(string $jobName = '') + private function startProfiling(string $jobName = ''): void { $this->statProfiler->clear(); $this->statProfiler->start( @@ -416,7 +417,7 @@ private function startProfiling(string $jobName = '') * @param string $jobName * @return void */ - private function stopProfiling(string $jobName = '') + private function stopProfiling(string $jobName = ''): void { $this->statProfiler->stop( sprintf(self::CRON_TIMERID, $jobName), @@ -445,9 +446,9 @@ private function getProfilingStat(string $jobName): string * Return job collection from data base with status 'pending'. * * @param string $groupId - * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection + * @return ScheduleCollection */ - private function getPendingSchedules($groupId) + private function getPendingSchedules(string $groupId): ScheduleCollection { $jobs = $this->_config->getJobs(); $pendingJobs = $this->_scheduleFactory->create()->getCollection(); @@ -462,7 +463,7 @@ private function getPendingSchedules($groupId) * @param string $groupId * @return $this */ - private function generateSchedules($groupId) + private function generateSchedules(string $groupId): self { /** * check if schedule generation is needed @@ -533,13 +534,13 @@ protected function _generateJobs($jobs, $exists, $groupId) * @param int $currentTime * @return void */ - private function cleanupJobs($groupId, $currentTime) + private function cleanupJobs(string $groupId, int $currentTime): void { // check if history cleanup is needed $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId); $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY); if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) { - return $this; + return; } // save time history cleanup was ran with no expiration $this->_cache->save( @@ -550,6 +551,7 @@ private function cleanupJobs($groupId, $currentTime) ); $this->cleanupDisabledJobs($groupId); + $this->cleanupRunningJobs($groupId); $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS); $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE); @@ -673,7 +675,7 @@ protected function getScheduleTimeInterval($groupId) * @param string $groupId * @return void */ - private function cleanupDisabledJobs($groupId) + private function cleanupDisabledJobs(string $groupId): void { $jobs = $this->_config->getJobs(); $jobsToCleanup = []; @@ -696,6 +698,33 @@ private function cleanupDisabledJobs($groupId) } } + /** + * Cleanup jobs that were left in a running state due to an unexpected stop + * + * @param string $groupId + * @return void + */ + private function cleanupRunningJobs(string $groupId): void + { + $scheduleResource = $this->_scheduleFactory->create()->getResource(); + $connection = $scheduleResource->getConnection(); + + $jobs = $this->_config->getJobs(); + + $connection->update( + $scheduleResource->getTable('cron_schedule'), + [ + 'status' => \Magento\Cron\Model\Schedule::STATUS_ERROR, + 'messages' => 'Time out' + ], + [ + $connection->quoteInto('status = ?', \Magento\Cron\Model\Schedule::STATUS_RUNNING), + $connection->quoteInto('job_code IN (?)', array_keys($jobs[$groupId])), + 'scheduled_at < UTC_TIMESTAMP() - INTERVAL 1 DAY' + ] + ); + } + /** * Get cron expression of cron job. * @@ -773,13 +802,13 @@ private function isGroupInFilter($groupId): bool * @param array $jobsRoot * @param int $currentTime */ - private function processPendingJobs($groupId, $jobsRoot, $currentTime) + private function processPendingJobs(string $groupId, array $jobsRoot, int $currentTime): void { - $procesedJobs = []; + $processedJobs = []; $pendingJobs = $this->getPendingSchedules($groupId); /** @var Schedule $schedule */ foreach ($pendingJobs as $schedule) { - if (isset($procesedJobs[$schedule->getJobCode()])) { + if (isset($processedJobs[$schedule->getJobCode()])) { // process only on job per run continue; } @@ -796,7 +825,7 @@ private function processPendingJobs($groupId, $jobsRoot, $currentTime) $this->tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId); if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) { - $procesedJobs[$schedule->getJobCode()] = true; + $processedJobs[$schedule->getJobCode()] = true; } $this->retrier->execute( @@ -821,7 +850,7 @@ private function tryRunJob($scheduledTime, $currentTime, $jobConfig, $schedule, { // use sha1 to limit length // phpcs:ignore Magento2.Security.InsecureFunction - $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); + $lockName = self::LOCK_PREFIX . md5($groupId . '_' . $schedule->getJobCode()); try { for ($retries = self::MAX_RETRIES; $retries > 0; $retries--) { diff --git a/app/code/Magento/Cron/Test/Unit/Model/DeadlockRetrierTest.php b/app/code/Magento/Cron/Test/Unit/Model/DeadlockRetrierTest.php index 60eaa091a761f..36e4537383aa6 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/DeadlockRetrierTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/DeadlockRetrierTest.php @@ -13,6 +13,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\DeadlockException; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; class DeadlockRetrierTest extends \PHPUnit\Framework\TestCase { @@ -27,6 +28,11 @@ class DeadlockRetrierTest extends \PHPUnit\Framework\TestCase */ private $adapterMock; + /** + * @var LoggerInterface|MockObject + */ + private $loggerMock; + /** * @var AbstractModel|MockObject */ @@ -38,8 +44,9 @@ class DeadlockRetrierTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { $this->adapterMock = $this->getMockForAbstractClass(AdapterInterface::class); + $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); $this->modelMock = $this->createMock(AbstractModel::class); - $this->retrier = new DeadlockRetrier(); + $this->retrier = new DeadlockRetrier($this->loggerMock); } /** @@ -75,6 +82,8 @@ public function testRetry(): void $this->modelMock->expects($this->exactly(DeadlockRetrierInterface::MAX_RETRIES)) ->method('getId') ->willThrowException(new DeadlockException()); + $this->loggerMock->expects($this->exactly(DeadlockRetrierInterface::MAX_RETRIES - 1)) + ->method('warning'); $this->retrier->execute( function () { @@ -95,6 +104,8 @@ public function testRetrySecond(): void $this->modelMock->expects($this->at(1)) ->method('getId') ->willReturn(2); + $this->loggerMock->expects($this->once()) + ->method('warning'); $this->retrier->execute( function () { diff --git a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php index a48a3dd76b884..50e1d828d2720 100644 --- a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php +++ b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php @@ -773,15 +773,10 @@ function ($callback) { ->setMethods(['execute'])->getMock(); $testCronJob->expects($this->atLeastOnce())->method('execute')->with($schedule); - $this->objectManagerMock->expects( - $this->once() - )->method( - 'create' - )->with( - 'CronJob' - )->willReturn( - $testCronJob - ); + $this->objectManagerMock->expects($this->once()) + ->method('create') + ->with('CronJob') + ->willReturn($testCronJob); $this->cronQueueObserver->execute($this->observerMock); } @@ -1049,9 +1044,36 @@ public function testMissedJobsCleanedInTime() $this->scheduleCollectionMock->expects($this->any())->method('load')->willReturnSelf(); $scheduleMock->expects($this->any())->method('getCollection')->willReturn($this->scheduleCollectionMock); - $scheduleMock->expects($this->exactly(9))->method('getResource')->willReturn($this->scheduleResourceMock); - $this->scheduleFactoryMock->expects($this->exactly(10))->method('create')->willReturn($scheduleMock); + $scheduleMock->expects($this->exactly(10))->method('getResource')->willReturn($this->scheduleResourceMock); + $this->scheduleFactoryMock->expects($this->exactly(11))->method('create')->willReturn($scheduleMock); + + $connectionMock = $this->prepareConnectionMock($tableName); + + $this->scheduleResourceMock->expects($this->exactly(6)) + ->method('getTable') + ->with($tableName) + ->willReturn($tableName); + $this->scheduleResourceMock->expects($this->exactly(15)) + ->method('getConnection') + ->willReturn($connectionMock); + + $this->retrierMock->expects($this->exactly(5)) + ->method('execute') + ->willReturnCallback( + function ($callback) { + return $callback(); + } + ); + + $this->cronQueueObserver->execute($this->observerMock); + } + /** + * @param string $tableName + * @return AdapterInterface|MockObject + */ + private function prepareConnectionMock(string $tableName) + { $connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); $connectionMock->expects($this->exactly(5)) @@ -1080,22 +1102,30 @@ public function testMissedJobsCleanedInTime() ) ->willReturn(1); - $this->scheduleResourceMock->expects($this->exactly(5)) - ->method('getTable') - ->with($tableName) - ->willReturn($tableName); - $this->scheduleResourceMock->expects($this->exactly(14)) - ->method('getConnection') - ->willReturn($connectionMock); - - $this->retrierMock->expects($this->exactly(5)) - ->method('execute') - ->willReturnCallback( - function ($callback) { - return $callback(); - } + $connectionMock->expects($this->any()) + ->method('quoteInto') + ->withConsecutive( + ['status = ?', \Magento\Cron\Model\Schedule::STATUS_RUNNING], + ['job_code IN (?)', ['test_job1']], + ) + ->willReturnOnConsecutiveCalls( + "status = 'running'", + "job_code IN ('test_job1')" ); - $this->cronQueueObserver->execute($this->observerMock); + $connectionMock->expects($this->once()) + ->method('update') + ->with( + $tableName, + ['status' => 'error', 'messages' => 'Time out'], + [ + "status = 'running'", + "job_code IN ('test_job1')", + 'scheduled_at < UTC_TIMESTAMP() - INTERVAL 1 DAY' + ] + ) + ->willReturn(0); + + return $connectionMock; } } diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml index 609b435f8b39c..72b1428756898 100644 --- a/app/code/Magento/Cron/etc/db_schema.xml +++ b/app/code/Magento/Cron/etc/db_schema.xml @@ -21,16 +21,10 @@ <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="schedule_id"/> </constraint> - <index referenceId="CRON_SCHEDULE_JOB_CODE" indexType="btree"> + <index referenceId="CRON_SCHEDULE_JOB_CODE_STATUS_SCHEDULED_AT" indexType="btree"> <column name="job_code"/> - </index> - <index referenceId="CRON_SCHEDULE_SCHEDULED_AT_STATUS" indexType="btree"> - <column name="scheduled_at"/> - <column name="status"/> - </index> - <index referenceId="CRON_SCHEDULE_SCHEDULE_ID_STATUS" indexType="btree"> - <column name="schedule_id"/> <column name="status"/> + <column name="scheduled_at"/> </index> </table> </schema> diff --git a/app/code/Magento/Cron/etc/db_schema_whitelist.json b/app/code/Magento/Cron/etc/db_schema_whitelist.json index f0d6ebed8290f..2e5cc6e0a4618 100644 --- a/app/code/Magento/Cron/etc/db_schema_whitelist.json +++ b/app/code/Magento/Cron/etc/db_schema_whitelist.json @@ -13,7 +13,8 @@ "index": { "CRON_SCHEDULE_JOB_CODE": true, "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true, - "CRON_SCHEDULE_SCHEDULE_ID_STATUS": true + "CRON_SCHEDULE_SCHEDULE_ID_STATUS": true, + "CRON_SCHEDULE_JOB_CODE_STATUS_SCHEDULED_AT": true }, "constraint": { "PRIMARY": true diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php index e0b3af9f9ed81..37590ab364832 100644 --- a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php @@ -36,13 +36,12 @@ public function convert($source) /** @var \DOMElement $value */ foreach ($policy->getElementsByTagName('value') as $value) { if ($value->attributes->getNamedItem('type')->nodeValue === 'host') { - $policyConfig[$id]['hosts'][] = $value->nodeValue; + $policyConfig[$id]['hosts'][$value->attributes->getNamedItem('id')->nodeValue] = $value->nodeValue; } else { $policyConfig[$id]['hashes'][$value->nodeValue] = $value->attributes->getNamedItem('algorithm')->nodeValue; } } - $policyConfig[$id]['hosts'] = array_unique($policyConfig[$id]['hosts']); } return $policyConfig; diff --git a/app/code/Magento/Csp/Model/Collector/FetchPolicyMerger.php b/app/code/Magento/Csp/Model/Collector/FetchPolicyMerger.php index 2a8f6c278b078..827e1aee6d0b9 100644 --- a/app/code/Magento/Csp/Model/Collector/FetchPolicyMerger.php +++ b/app/code/Magento/Csp/Model/Collector/FetchPolicyMerger.php @@ -25,12 +25,12 @@ public function merge(PolicyInterface $policy1, PolicyInterface $policy2): Polic return new FetchPolicy( $policy1->getId(), $policy1->isNoneAllowed() || $policy2->isNoneAllowed(), - array_unique(array_merge($policy1->getHostSources(), $policy2->getHostSources())), - array_unique(array_merge($policy1->getSchemeSources(), $policy2->getSchemeSources())), + array_merge($policy1->getHostSources(), $policy2->getHostSources()), + array_merge($policy1->getSchemeSources(), $policy2->getSchemeSources()), $policy1->isSelfAllowed() || $policy2->isSelfAllowed(), $policy1->isInlineAllowed() || $policy2->isInlineAllowed(), $policy1->isEvalAllowed() || $policy2->isEvalAllowed(), - array_unique(array_merge($policy1->getNonceValues(), $policy2->getNonceValues())), + array_merge($policy1->getNonceValues(), $policy2->getNonceValues()), array_merge($policy1->getHashes(), $policy2->getHashes()), $policy1->isDynamicAllowed() || $policy2->isDynamicAllowed(), $policy1->areEventHandlersAllowed() || $policy2->areEventHandlersAllowed() diff --git a/app/code/Magento/Csp/Model/Collector/PluginTypesPolicyMerger.php b/app/code/Magento/Csp/Model/Collector/PluginTypesPolicyMerger.php index 58f2128657788..8e70d1f851479 100644 --- a/app/code/Magento/Csp/Model/Collector/PluginTypesPolicyMerger.php +++ b/app/code/Magento/Csp/Model/Collector/PluginTypesPolicyMerger.php @@ -22,7 +22,7 @@ public function merge(PolicyInterface $policy1, PolicyInterface $policy2): Polic { /** @var PluginTypesPolicy $policy1 */ /** @var PluginTypesPolicy $policy2 */ - return new PluginTypesPolicy(array_unique(array_merge($policy1->getTypes(), $policy2->getTypes()))); + return new PluginTypesPolicy(array_merge($policy1->getTypes(), $policy2->getTypes())); } /** diff --git a/app/code/Magento/Csp/Model/Policy/FetchPolicy.php b/app/code/Magento/Csp/Model/Policy/FetchPolicy.php index d045ee48b0ba2..2ed69a070d73f 100644 --- a/app/code/Magento/Csp/Model/Policy/FetchPolicy.php +++ b/app/code/Magento/Csp/Model/Policy/FetchPolicy.php @@ -116,12 +116,12 @@ public function __construct( ) { $this->id = $id; $this->noneAllowed = $noneAllowed; - $this->hostSources = array_unique($hostSources); - $this->schemeSources = array_unique($schemeSources); + $this->hostSources = array_values(array_unique($hostSources)); + $this->schemeSources = array_values(array_unique($schemeSources)); $this->selfAllowed = $selfAllowed; $this->inlineAllowed = $inlineAllowed; $this->evalAllowed = $evalAllowed; - $this->nonceValues = array_unique($nonceValues); + $this->nonceValues = array_values(array_unique($nonceValues)); $this->hashes = $hashValues; $this->dynamicAllowed = $dynamicAllowed; $this->eventHandlersAllowed = $eventHandlersAllowed; diff --git a/app/code/Magento/Csp/Model/Policy/PluginTypesPolicy.php b/app/code/Magento/Csp/Model/Policy/PluginTypesPolicy.php index 4f34f49bfffe7..d30f4e5afef22 100644 --- a/app/code/Magento/Csp/Model/Policy/PluginTypesPolicy.php +++ b/app/code/Magento/Csp/Model/Policy/PluginTypesPolicy.php @@ -25,7 +25,7 @@ public function __construct(array $types) if (!$types) { throw new \RuntimeException('PluginTypePolicy must be given at least 1 type.'); } - $this->types = array_unique($types); + $this->types = array_values(array_unique($types)); } /** diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index 6b59986f8ec5f..c2137f1b40019 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -31,6 +31,8 @@ use Magento\Framework\Exception\State\UserLockedException; use Magento\Customer\Controller\AbstractAccount; use Magento\Framework\Phrase; +use Magento\Framework\Filesystem; +use Magento\Framework\App\Filesystem\DirectoryList; /** * Customer edit account information controller @@ -94,6 +96,11 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, Http */ private $addressRegistry; + /** + * @var Filesystem + */ + private $filesystem; + /** * @param Context $context * @param Session $customerSession @@ -103,6 +110,7 @@ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, Http * @param CustomerExtractor $customerExtractor * @param Escaper|null $escaper * @param AddressRegistry|null $addressRegistry + * @param Filesystem $filesystem */ public function __construct( Context $context, @@ -112,7 +120,8 @@ public function __construct( Validator $formKeyValidator, CustomerExtractor $customerExtractor, ?Escaper $escaper = null, - AddressRegistry $addressRegistry = null + AddressRegistry $addressRegistry = null, + Filesystem $filesystem = null ) { parent::__construct($context); $this->session = $customerSession; @@ -122,6 +131,7 @@ public function __construct( $this->customerExtractor = $customerExtractor; $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); $this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class); + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** @@ -201,6 +211,14 @@ public function execute() $currentCustomerDataObject ); + $attributeToDelete = $this->_request->getParam('delete_attribute_value'); + if ($attributeToDelete !== null) { + $this->deleteCustomerFileAttribute( + $customerCandidateDataObject, + $attributeToDelete + ); + } + try { // whether a customer enabled change email option $this->processChangeEmailRequest($currentCustomerDataObject); @@ -388,4 +406,41 @@ private function disableAddressValidation($customer) $addressModel->setShouldIgnoreValidation(true); } } + + /** + * Removes file attribute from customer entity and file from filesystem + * + * @param CustomerInterface $customerCandidateDataObject + * @param string $attributeToDelete + * @return void + */ + private function deleteCustomerFileAttribute( + CustomerInterface $customerCandidateDataObject, + string $attributeToDelete + ) : void { + if ($attributeToDelete !== '') { + if (strpos($attributeToDelete, ',') !== false) { + $attributes = explode(',', $attributeToDelete); + } else { + $attributes[] = $attributeToDelete; + } + foreach ($attributes as $attr) { + $attributeValue = $customerCandidateDataObject->getCustomAttribute($attr); + if ($attributeValue!== null) { + if ($attributeValue->getValue() !== '') { + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $fileName = $attributeValue->getValue(); + $path = $mediaDirectory->getAbsolutePath('customer' . $fileName); + if ($fileName && $mediaDirectory->isFile($path)) { + $mediaDirectory->delete($path); + } + $customerCandidateDataObject->setCustomAttribute( + $attr, + '' + ); + } + } + } + } + } } diff --git a/app/code/Magento/Customer/Controller/Address/FormPost.php b/app/code/Magento/Customer/Controller/Address/FormPost.php index 25618e3129160..cae039ea975b8 100644 --- a/app/code/Magento/Customer/Controller/Address/FormPost.php +++ b/app/code/Magento/Customer/Controller/Address/FormPost.php @@ -24,6 +24,9 @@ use Magento\Framework\Exception\InputException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\View\Result\PageFactory; +use Magento\Framework\Filesystem; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NotFoundException; /** * Customer Address Form Post Controller @@ -47,6 +50,11 @@ class FormPost extends \Magento\Customer\Controller\Address implements HttpPostA */ private $customerAddressMapper; + /** + * @var Filesystem + */ + private $filesystem; + /** * @param Context $context * @param Session $customerSession @@ -61,6 +69,7 @@ class FormPost extends \Magento\Customer\Controller\Address implements HttpPostA * @param PageFactory $resultPageFactory * @param RegionFactory $regionFactory * @param HelperData $helperData + * @param Filesystem $filesystem * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -76,10 +85,12 @@ public function __construct( ForwardFactory $resultForwardFactory, PageFactory $resultPageFactory, RegionFactory $regionFactory, - HelperData $helperData + HelperData $helperData, + Filesystem $filesystem = null ) { $this->regionFactory = $regionFactory; $this->helperData = $helperData; + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); parent::__construct( $context, $customerSession, @@ -150,7 +161,7 @@ protected function getExistingAddressData() if ($addressId = $this->getRequest()->getParam('id')) { $existingAddress = $this->_addressRepository->getById($addressId); if ($existingAddress->getCustomerId() !== $this->_getSession()->getCustomerId()) { - throw new \Exception(); + throw new NotFoundException(__('Address not found.')); } $existingAddressData = $this->getCustomerAddressMapper()->toFlatArray($existingAddress); } @@ -210,6 +221,9 @@ public function execute() try { $address = $this->_extractAddress(); + if ($this->_request->getParam('delete_attribute_value')) { + $address = $this->deleteAddressFileAttribute($address); + } $this->_addressRepository->save($address); $this->messageManager->addSuccessMessage(__('You saved the address.')); $url = $this->_buildUrl('*/*/index', ['_secure' => true]); @@ -249,4 +263,31 @@ private function getCustomerAddressMapper() } return $this->customerAddressMapper; } + + /** + * Removes file attribute from customer address and file from filesystem + * + * @param \Magento\Customer\Api\Data\AddressInterface $address + * @return mixed + */ + private function deleteAddressFileAttribute($address) + { + $attributeValue = $address->getCustomAttribute($this->_request->getParam('delete_attribute_value')); + if ($attributeValue!== null) { + if ($attributeValue->getValue() !== '') { + $mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $fileName = $attributeValue->getValue(); + $path = $mediaDirectory->getAbsolutePath('customer_address' . $fileName); + if ($fileName && $mediaDirectory->isFile($path)) { + $mediaDirectory->delete($path); + } + $address->setCustomAttribute( + $this->_request->getParam('delete_attribute_value'), + '' + ); + } + } + + return $address; + } } diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index 192a0f1362ecd..cc8531214c7da 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Backend\App\Action\Context; @@ -40,6 +41,7 @@ use Magento\Framework\Math\Random; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Registry; +use Magento\Framework\Validator\Exception; use Magento\Framework\View\Result\LayoutFactory; use Magento\Framework\View\Result\PageFactory; use Magento\Newsletter\Model\SubscriberFactory; @@ -243,10 +245,10 @@ protected function _extractData( /** * Saves default_billing and default_shipping flags for customer address * - * @deprecated 102.0.1 must be removed because addresses are save separately for now * @param array $addressIdList * @param array $extractedCustomerData * @return array + * @deprecated 102.0.1 must be removed because addresses are save separately for now */ protected function saveDefaultFlags(array $addressIdList, array &$extractedCustomerData) { @@ -286,9 +288,9 @@ protected function saveDefaultFlags(array $addressIdList, array &$extractedCusto /** * Reformat customer addresses data to be compatible with customer service interface * - * @deprecated 102.0.1 addresses are saved separately for now * @param array $extractedCustomerData * @return array + * @deprecated 102.0.1 addresses are saved separately for now */ protected function _extractCustomerAddressData(array &$extractedCustomerData) { @@ -318,6 +320,7 @@ public function execute() { $returnToEdit = false; $customerId = $this->getCurrentCustomerId(); + $customer = $this->customerDataFactory->create(); if ($this->getRequest()->getPostValue()) { try { @@ -335,8 +338,6 @@ public function execute() $customerData['id'] = $customerId; } - /** @var CustomerInterface $customer */ - $customer = $this->customerDataFactory->create(); $this->dataObjectHelper->populateWithArray( $customer, $customerData, @@ -353,7 +354,7 @@ public function execute() try { $this->customerAccountManagement->validateCustomerStoreIdByWebsiteId($customer); } catch (LocalizedException $exception) { - throw new LocalizedException(__("The Store View selected for sending Welcome email from". + throw new LocalizedException(__("The Store View selected for sending Welcome email from" . " is not related to the customer's associated website.")); } } @@ -361,7 +362,6 @@ public function execute() // Save customer if ($customerId) { $this->_customerRepository->save($customer); - $this->getEmailNotification()->credentialsChanged($customer, $currentCustomer->getEmail()); } else { $customer = $this->customerAccountManagement->createAccount($customer); @@ -386,13 +386,13 @@ public function execute() __('Something went wrong while saving the customer.') ); $returnToEdit = false; - } catch (\Magento\Framework\Validator\Exception $exception) { + } catch (Exception $exception) { $messages = $exception->getMessages(); if (empty($messages)) { $messages = $exception->getMessage(); } $this->_addSessionErrorMessages($messages); - $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData($customer)); $returnToEdit = true; } catch (AbstractAggregateException $exception) { $errors = $exception->getErrors(); @@ -401,18 +401,18 @@ public function execute() $messages[] = $error->getMessage(); } $this->_addSessionErrorMessages($messages); - $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData($customer)); $returnToEdit = true; } catch (LocalizedException $exception) { $this->_addSessionErrorMessages($exception->getMessage()); - $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData($customer)); $returnToEdit = true; } catch (\Exception $exception) { $this->messageManager->addExceptionMessage( $exception, __('Something went wrong while saving the customer.') ); - $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData()); + $this->_getSession()->setCustomerFormData($this->retrieveFormattedFormData($customer)); $returnToEdit = true; } } @@ -553,21 +553,16 @@ private function disableAddressValidation($customer) /** * Retrieve formatted form data * + * @param CustomerInterface $customer * @return array */ - private function retrieveFormattedFormData(): array + private function retrieveFormattedFormData(CustomerInterface $customer): array { $originalRequestData = $this->getRequest()->getPostValue(); + $customerData = $this->customerMapper->toFlatArray($customer); /* Customer data filtration */ if (isset($originalRequestData['customer'])) { - $customerData = $this->_extractData( - 'adminhtml_customer', - CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, - [], - 'customer' - ); - $customerData = array_intersect_key($customerData, $originalRequestData['customer']); $originalRequestData['customer'] = array_merge($originalRequestData['customer'], $customerData); } diff --git a/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php b/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php index d6e63e11ee453..0fd72a591899a 100644 --- a/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php +++ b/app/code/Magento/Customer/Model/Address/CustomAttributesProcessor.php @@ -71,7 +71,7 @@ private function getAttributeLabels(array $customAttribute, string $customAttrib { $attributeOptionLabels = []; - if (!empty($customAttribute['value'])) { + if (isset($customAttribute['value']) && $customAttribute['value'] != null) { $customAttributeValues = explode(',', $customAttribute['value']); $attributeOptions = $this->attributeOptionManager->getItems( \Magento\Customer\Model\Indexer\Address\AttributeProvider::ENTITY, diff --git a/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php b/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php index ed84b0204e8ba..a3617ac4e4e79 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php +++ b/app/code/Magento/Customer/Model/Customer/DataProviderWithDefaultAddresses.php @@ -8,11 +8,13 @@ use Magento\Customer\Model\Address; use Magento\Customer\Model\Customer; +use Magento\Customer\Model\CustomerFactory; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; use Magento\Directory\Model\CountryFactory; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Eav\Model\Entity\Type; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Session\SessionManagerInterface; use Magento\Customer\Model\FileUploaderDataResolver; @@ -67,6 +69,11 @@ class DataProviderWithDefaultAddresses extends AbstractDataProvider */ private $attributeMetadataResolver; + /** + * @var CustomerFactory + */ + private $customerFactory; + /** * @param string $name * @param string $primaryFieldName @@ -80,6 +87,7 @@ class DataProviderWithDefaultAddresses extends AbstractDataProvider * @param bool $allowToShowHiddenAttributes * @param array $meta * @param array $data + * @param CustomerFactory $customerFactory * @throws LocalizedException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -95,7 +103,8 @@ public function __construct( AttributeMetadataResolver $attributeMetadataResolver, $allowToShowHiddenAttributes = true, array $meta = [], - array $data = [] + array $data = [], + CustomerFactory $customerFactory = null ) { parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); $this->collection = $customerCollectionFactory->create(); @@ -108,6 +117,7 @@ public function __construct( $this->meta['customer']['children'] = $this->getAttributesMeta( $eavConfig->getEntityType('customer') ); + $this->customerFactory = $customerFactory ?: ObjectManager::getInstance()->get(CustomerFactory::class); } /** @@ -144,9 +154,10 @@ public function getData(): array $this->loadedData[$customer->getId()] = $result; } - $data = $this->session->getCustomerFormData(); if (!empty($data)) { + $customer = $this->customerFactory->create(); + $this->fileUploaderDataResolver->overrideFileUploaderData($customer, $data['customer']); $customerId = $data['customer']['entity_id'] ?? null; $this->loadedData[$customerId] = $data; $this->session->unsCustomerFormData(); diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index c596f8c313ab3..02bfe78be535c 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -7,6 +7,18 @@ namespace Magento\Customer\Model; +use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\UrlInterface; +use Magento\MediaStorage\Model\File\Uploader; +use Magento\MediaStorage\Model\File\UploaderFactory; + /** * Processor class for work with uploaded files */ @@ -18,22 +30,22 @@ class FileProcessor const TMP_DIR = 'tmp'; /** - * @var \Magento\Framework\Filesystem\Directory\WriteInterface + * @var WriteInterface */ private $mediaDirectory; /** - * @var \Magento\MediaStorage\Model\File\UploaderFactory + * @var UploaderFactory */ private $uploaderFactory; /** - * @var \Magento\Framework\UrlInterface + * @var UrlInterface */ private $urlBuilder; /** - * @var \Magento\Framework\Url\EncoderInterface + * @var EncoderInterface */ private $urlEncoder; @@ -48,29 +60,29 @@ class FileProcessor private $allowedExtensions = []; /** - * @var \Magento\Framework\File\Mime + * @var Mime */ private $mime; /** - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory - * @param \Magento\Framework\UrlInterface $urlBuilder - * @param \Magento\Framework\Url\EncoderInterface $urlEncoder + * @param Filesystem $filesystem + * @param UploaderFactory $uploaderFactory + * @param UrlInterface $urlBuilder + * @param EncoderInterface $urlEncoder * @param string $entityTypeCode - * @param \Magento\Framework\File\Mime $mime + * @param Mime $mime * @param array $allowedExtensions */ public function __construct( - \Magento\Framework\Filesystem $filesystem, - \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory, - \Magento\Framework\UrlInterface $urlBuilder, - \Magento\Framework\Url\EncoderInterface $urlEncoder, + Filesystem $filesystem, + UploaderFactory $uploaderFactory, + UrlInterface $urlBuilder, + EncoderInterface $urlEncoder, $entityTypeCode, - \Magento\Framework\File\Mime $mime, + Mime $mime, array $allowedExtensions = [] ) { - $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); + $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->uploaderFactory = $uploaderFactory; $this->urlBuilder = $urlBuilder; $this->urlEncoder = $urlEncoder; @@ -91,8 +103,7 @@ public function getBase64EncodedData($fileName) $fileContent = $this->mediaDirectory->readFile($filePath); - $encodedContent = base64_encode($fileContent); - return $encodedContent; + return base64_encode($fileContent); } /** @@ -105,8 +116,7 @@ public function getStat($fileName) { $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); - $result = $this->mediaDirectory->stat($filePath); - return $result; + return $this->mediaDirectory->stat($filePath); } /** @@ -120,8 +130,7 @@ public function getMimeType($fileName) $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($filePath); - $result = $this->mime->getMimeType($absoluteFilePath); - return $result; + return $this->mime->getMimeType($absoluteFilePath); } /** @@ -134,8 +143,7 @@ public function isExist($fileName) { $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); - $result = $this->mediaDirectory->isExist($filePath); - return $result; + return $this->mediaDirectory->isExist($filePath); } /** @@ -149,13 +157,13 @@ public function getViewUrl($filePath, $type) { $viewUrl = ''; - if ($this->entityTypeCode == \Magento\Customer\Api\AddressMetadataInterface::ENTITY_TYPE_ADDRESS) { + if ($this->entityTypeCode == AddressMetadataInterface::ENTITY_TYPE_ADDRESS) { $filePath = $this->entityTypeCode . '/' . ltrim($filePath, '/'); - $viewUrl = $this->urlBuilder->getBaseUrl(['_type' => \Magento\Framework\UrlInterface::URL_TYPE_MEDIA]) + $viewUrl = $this->urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]) . $this->mediaDirectory->getRelativePath($filePath); } - if ($this->entityTypeCode == \Magento\Customer\Api\CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER) { + if ($this->entityTypeCode == CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER) { $viewUrl = $this->urlBuilder->getUrl( 'customer/index/viewfile', [$type => $this->urlEncoder->encode(ltrim($filePath, '/'))] @@ -170,11 +178,11 @@ public function getViewUrl($filePath, $type) * * @param string $fileId * @return \string[] - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function saveTemporaryFile($fileId) { - /** @var \Magento\MediaStorage\Model\File\Uploader $uploader */ + /** @var Uploader $uploader */ $uploader = $this->uploaderFactory->create(['fileId' => $fileId]); $uploader->setFilesDispersion(false); $uploader->setFilenamesCaseSensitivity(false); @@ -188,7 +196,7 @@ public function saveTemporaryFile($fileId) $result = $uploader->save($path); unset($result['path']); if (!$result) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('File can not be saved to the destination folder.') ); } @@ -201,28 +209,32 @@ public function saveTemporaryFile($fileId) * * @param string $fileName * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function moveTemporaryFile($fileName) { + if (!$this->isFileTemporary($fileName)) { + return $fileName; + } + $fileName = ltrim($fileName, '/'); - $dispersionPath = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); + $dispersionPath = Uploader::getDispersionPath($fileName); $destinationPath = $this->entityTypeCode . $dispersionPath; if (!$this->mediaDirectory->create($destinationPath)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Unable to create directory %1.', $destinationPath) ); } if (!$this->mediaDirectory->isWritable($destinationPath)) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Destination folder is not writable or does not exists.') ); } - $destinationFileName = \Magento\MediaStorage\Model\File\Uploader::getNewFileName( + $destinationFileName = Uploader::getNewFileName( $this->mediaDirectory->getAbsolutePath($destinationPath) . '/' . $fileName ); @@ -238,8 +250,7 @@ public function moveTemporaryFile($fileName) ); } - $fileName = $dispersionPath . '/' . $destinationFileName; - return $fileName; + return $dispersionPath . '/' . $destinationFileName; } /** @@ -252,7 +263,20 @@ public function removeUploadedFile($fileName) { $filePath = $this->entityTypeCode . '/' . ltrim($fileName, '/'); - $result = $this->mediaDirectory->delete($filePath); - return $result; + return $this->mediaDirectory->delete($filePath); + } + + /** + * Verify if given file temporary. + * + * @param string $fileName + * @return bool + */ + private function isFileTemporary(string $fileName): bool + { + $tmpFile = $this->entityTypeCode . '/' . self::TMP_DIR . '/' . ltrim($fileName, '/'); + $destinationFile = $this->entityTypeCode . '/' . ltrim($fileName, '/'); + + return $this->mediaDirectory->isExist($tmpFile) && !$this->mediaDirectory->isExist($destinationFile); } } diff --git a/app/code/Magento/Customer/Model/FileUploader.php b/app/code/Magento/Customer/Model/FileUploader.php index c425ac06666c5..411ab37a1d740 100644 --- a/app/code/Magento/Customer/Model/FileUploader.php +++ b/app/code/Magento/Customer/Model/FileUploader.php @@ -100,14 +100,33 @@ public function validate() * @throws LocalizedException */ public function upload() + { + return $this->uploadFile(); + } + + /** + * File uploading process + * + * @param bool $useScope + * @return string[] + * @throws LocalizedException + */ + public function uploadFile($useScope = true) { /** @var FileProcessor $fileProcessor */ - $fileProcessor = $this->fileProcessorFactory->create([ - 'entityTypeCode' => $this->entityTypeCode, - 'allowedExtensions' => $this->getAllowedExtensions(), - ]); + $fileProcessor = $this->fileProcessorFactory->create( + [ + 'entityTypeCode' => $this->entityTypeCode, + 'allowedExtensions' => $this->getAllowedExtensions(), + ] + ); - $result = $fileProcessor->saveTemporaryFile($this->scope . '[' . $this->getAttributeCode() . ']'); + if ($useScope === true) { + $fileId = $this->scope . '[' . $this->getAttributeCode() . ']'; + } else { + $fileId = $this->getAttributeCode(); + } + $result = $fileProcessor->saveTemporaryFile($fileId); // Update tmp_name param. Required for attribute validation! $result['tmp_name'] = ltrim($result['file'], '/'); @@ -127,7 +146,14 @@ public function upload() */ private function getAttributeCode() { - return key($_FILES[$this->scope]['name']); + // phpcs:disable Magento2.Security.Superglobal + if (is_array($_FILES[$this->scope]['name'])) { + $code = key($_FILES[$this->scope]['name']); + } else { + $code = $this->scope; + } + // phpcs:enable Magento2.Security.Superglobal + return $code; } /** @@ -139,10 +165,16 @@ private function getData() { $data = []; + // phpcs:disable Magento2.Security.Superglobal $fileAttributes = $_FILES[$this->scope]; foreach ($fileAttributes as $attributeName => $attributeValue) { - $data[$attributeName] = $attributeValue[$this->getAttributeCode()]; + if (is_array($attributeValue)) { + $data[$attributeName] = $attributeValue[$this->getAttributeCode()]; + } else { + $data[$attributeName] = $attributeValue; + } } + // phpcs:enable Magento2.Security.Superglobal return $data; } @@ -160,9 +192,12 @@ private function getAllowedExtensions() foreach ($validationRules as $validationRule) { if ($validationRule->getName() == 'file_extensions') { $allowedExtensions = explode(',', $validationRule->getValue()); - array_walk($allowedExtensions, function (&$value) { - $value = strtolower(trim($value)); - }); + array_walk( + $allowedExtensions, + function (&$value) { + $value = strtolower(trim($value)); + } + ); break; } } diff --git a/app/code/Magento/Customer/Model/Group/Resolver.php b/app/code/Magento/Customer/Model/Group/Resolver.php new file mode 100644 index 0000000000000..fd797d744e6dc --- /dev/null +++ b/app/code/Magento/Customer/Model/Group/Resolver.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Group; + +use Magento\Customer\Model\ResourceModel\Group\Resolver as ResolverResource; + +/** + * Lightweight service for getting current customer group based on customer id + */ +class Resolver +{ + /** + * @var ResolverResource + */ + private $resolverResource; + + /** + * @param ResolverResource $resolverResource + */ + public function __construct(ResolverResource $resolverResource) + { + $this->resolverResource = $resolverResource; + } + + /** + * Return customer group id + * + * @param int $customerId + * @return int|null + */ + public function resolve(int $customerId) : ?int + { + return $this->resolverResource->resolve($customerId); + } +} diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php index 1a1c48075fce5..17cfc0325ef41 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/File.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php @@ -13,6 +13,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\File\UploaderFactory; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File as IoFile; /** * Processes files that are save for customer. @@ -61,6 +62,11 @@ class File extends AbstractData */ protected $fileProcessorFactory; + /** + * @var IoFile|null + */ + private $ioFile; + /** * Constructor * @@ -76,6 +82,7 @@ class File extends AbstractData * @param Filesystem $fileSystem * @param UploaderFactory $uploaderFactory * @param \Magento\Customer\Model\FileProcessorFactory|null $fileProcessorFactory + * @param IoFile|null $ioFile * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -90,7 +97,8 @@ public function __construct( \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator, Filesystem $fileSystem, UploaderFactory $uploaderFactory, - \Magento\Customer\Model\FileProcessorFactory $fileProcessorFactory = null + FileProcessorFactory $fileProcessorFactory = null, + IoFile $ioFile = null ) { parent::__construct($localeDate, $logger, $attribute, $localeResolver, $value, $entityTypeCode, $isAjax); $this->urlEncoder = $urlEncoder; @@ -98,8 +106,10 @@ public function __construct( $this->_fileSystem = $fileSystem; $this->uploaderFactory = $uploaderFactory; $this->fileProcessorFactory = $fileProcessorFactory ?: ObjectManager::getInstance() - ->get(\Magento\Customer\Model\FileProcessorFactory::class); + ->get(FileProcessorFactory::class); $this->fileProcessor = $this->fileProcessorFactory->create(['entityTypeCode' => $this->_entityTypeCode]); + $this->ioFile = $ioFile ?: ObjectManager::getInstance() + ->get(IoFile::class); } /** @@ -110,11 +120,17 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) { $extend = $this->_getRequestValue($request); + // phpcs:disable Magento2.Security.Superglobal $attrCode = $this->getAttribute()->getAttributeCode(); - if ($this->_requestScope || !isset($_FILES[$attrCode])) { + + // phpcs:disable Magento2.Security.Superglobal + $uploadedFile = $request->getParam($attrCode . '_uploaded'); + if ($uploadedFile) { + $value = $uploadedFile; + } elseif ($this->_requestScope || !isset($_FILES[$attrCode])) { $value = []; - if (strpos($this->_requestScope, '/') !== false) { - $scopes = explode('/', $this->_requestScope); + if (strpos($this->_requestScope, DIRECTORY_SEPARATOR) !== false) { + $scopes = explode(DIRECTORY_SEPARATOR, $this->_requestScope); $mainScope = array_shift($scopes); } else { $mainScope = $this->_requestScope; @@ -153,6 +169,7 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request) $value = []; } } + // phpcs:enable Magento2.Security.Superglobal if (!empty($extend['delete'])) { $value['delete'] = true; @@ -171,7 +188,9 @@ protected function _validateByRules($value) { $label = $value['name']; $rules = $this->getAttribute()->getValidationRules(); - $extension = pathinfo($value['name'], PATHINFO_EXTENSION); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $pathInfo = $this->ioFile->getPathInfo($label); + $extension = $pathInfo['extension'] ?? null; $fileExtensions = ArrayObjectSearch::getArrayElementByName( $rules, 'file_extensions' @@ -219,12 +238,14 @@ protected function _validateByRules($value) */ protected function _isUploadedFile($filename) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction if (is_uploaded_file($filename)) { return true; } // This case is required for file uploader UI component - $temporaryFile = FileProcessor::TMP_DIR . '/' . pathinfo($filename)['basename']; + $temporaryFile = FileProcessor::TMP_DIR . DIRECTORY_SEPARATOR . + $this->ioFile->getPathInfo($filename)['basename']; if ($this->fileProcessor->isExist($temporaryFile)) { return true; } @@ -290,10 +311,9 @@ public function compactValue($value) return $value; } - if (isset($value['file']) && !empty($value['file'])) { - if ($value['file'] == $this->_value) { - return $this->_value; - } + if ($value && is_string($value) && $this->fileProcessor->isExist($value)) { + $result = $value; + } elseif (isset($value['file']) && !empty($value['file'])) { $result = $this->processUiComponentValue($value); } else { $result = $this->processInputFieldValue($value); @@ -310,6 +330,9 @@ public function compactValue($value) */ protected function processUiComponentValue(array $value) { + if ($value['file'] == $this->_value) { + return $this->_value; + } $result = $this->fileProcessor->moveTemporaryFile($value['file']); return $result; } @@ -338,7 +361,8 @@ protected function processInputFieldValue($value) $result = $this->_value; if ($toDelete) { - $mediaDir->delete($this->_entityTypeCode . '/' . ltrim($this->_value, '/')); + $mediaDir->delete($this->_entityTypeCode . DIRECTORY_SEPARATOR . + ltrim($this->_value, DIRECTORY_SEPARATOR)); $result = ''; } @@ -363,7 +387,10 @@ protected function processInputFieldValue($value) */ public function restoreValue($value) { - return $this->_value; + if (!empty($this->_value)) { + return $this->_value; + } + return $this->compactValue($value); } /** diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Image.php b/app/code/Magento/Customer/Model/Metadata/Form/Image.php index d023db1454906..b5bfe00c23384 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Image.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Image.php @@ -16,15 +16,15 @@ use Magento\Framework\Api\ArrayObjectSearch; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\UploaderFactory; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteFactory; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\Io\File as IoFileSystem; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Filesystem\Directory\WriteFactory; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\Url\EncoderInterface; @@ -54,8 +54,6 @@ class Image extends File private $mediaEntityTmpDirectory; /** - * Constructor - * * @param TimezoneInterface $localeDate * @param LoggerInterface $logger * @param AttributeMetadataInterface $attribute @@ -207,13 +205,11 @@ protected function _validateByRules($value) protected function processUiComponentValue(array $value) { if ($this->_entityTypeCode == AddressMetadataInterface::ENTITY_TYPE_ADDRESS) { - $result = $this->processCustomerAddressValue($value); - return $result; + return $this->processCustomerAddressValue($value); } if ($this->_entityTypeCode == CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER) { - $result = $this->processCustomerValue($value); - return $result; + return $this->processCustomerValue($value); } return $this->_value; @@ -267,6 +263,6 @@ protected function processCustomerValue(array $value) return $imageContentDataObject; } - return $this->_value; + return $this->_value ?: $value['file']; } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Group/Resolver.php b/app/code/Magento/Customer/Model/ResourceModel/Group/Resolver.php new file mode 100644 index 0000000000000..82c2cf2449cd3 --- /dev/null +++ b/app/code/Magento/Customer/Model/ResourceModel/Group/Resolver.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\ResourceModel\Group; + +use Magento\Framework\App\ResourceConnection; + +/** + * Resource model for customer group resolver service + */ +class Resolver +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Resolve customer group from db + * + * @param int $customerId + * @return int|null + */ + public function resolve(int $customerId) : ?int + { + $result = null; + + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('customer_entity'); + + $query = $connection + ->select() + ->from( + ['main_table' => $tableName], + ['main_table.group_id'] + ) + ->where('main_table.entity_id = ?', $customerId); + $groupId = $connection->fetchOne($query); + if ($groupId) { + $result = (int) $groupId; + } + + return $result; + } +} diff --git a/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php b/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php index af0a04827d30f..33290306e4843 100644 --- a/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php +++ b/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php @@ -141,7 +141,8 @@ public function execute(Observer $observer) if ($customerAddress->getVatId() == '' || !$this->_customerVat->isCountryInEU($customerAddress->getCountry()) ) { - $defaultGroupId = $this->_groupManagement->getDefaultGroup($customer->getStore())->getId(); + $defaultGroupId = $customer->getGroupId() ? $customer->getGroupId() : + $this->_groupManagement->getDefaultGroup($customer->getStore())->getId(); if (!$customer->getDisableAutoGroupChange() && $customer->getGroupId() != $defaultGroupId) { $customer->setGroupId($defaultGroupId); $customer->save(); @@ -216,8 +217,8 @@ protected function _canProcessAddress($address) protected function _isDefaultBilling($address) { return $address->getId() && $address->getId() == $address->getCustomer()->getDefaultBilling() - || $address->getIsPrimaryBilling() - || $address->getIsDefaultBilling(); + || $address->getIsPrimaryBilling() + || $address->getIsDefaultBilling(); } /** @@ -229,8 +230,8 @@ protected function _isDefaultBilling($address) protected function _isDefaultShipping($address) { return $address->getId() && $address->getId() == $address->getCustomer()->getDefaultShipping() - || $address->getIsPrimaryShipping() - || $address->getIsDefaultShipping(); + || $address->getIsPrimaryShipping() + || $address->getIsDefaultShipping(); } /** diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillCustomerMainDataActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillCustomerMainDataActionGroup.xml new file mode 100644 index 0000000000000..dc7d68faf362d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillCustomerMainDataActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillCustomerMainDataActionGroup"> + <annotations> + <description>Fill customer main required data. Starts on customer creation/edit page.</description> + </annotations> + + <arguments> + <argument name="firstName" type="string" defaultValue="{{Simple_US_Customer.firstname}}"/> + <argument name="lastName" type="string" defaultValue="{{Simple_US_Customer.lastname}}"/> + <argument name="email" type="string" defaultValue="{{Simple_US_Customer.email}}"/> + </arguments> + <waitForElementVisible selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="waitForCustomerPageLoad"/> + <fillField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{firstName}}" stepKey="fillFirstName"/> + <fillField selector="{{AdminCustomerAccountInformationSection.lastName}}" userInput="{{lastName}}" stepKey="fillLastName"/> + <fillField selector="{{AdminCustomerAccountInformationSection.email}}" userInput="{{email}}" stepKey="fillEmail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenAddressesTabFromCustomerEditPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenAddressesTabFromCustomerEditPageActionGroup.xml new file mode 100644 index 0000000000000..5c9d314ca728a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenAddressesTabFromCustomerEditPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenAddressesTabFromCustomerEditPageActionGroup"> + <annotations> + <description>Open the Addresses tab from a Customer's edit page in admin</description> + </annotations> + <waitForElementVisible selector="{{AdminCustomerInformationSection.addresses}}" stepKey="waitForAddressesTab"/> + <click selector="{{AdminCustomerInformationSection.addresses}}" stepKey="clickAddressesTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{AdminCustomerAddressesGridSection.customerAddressGrid}}" stepKey="waitForCustomerAddressesGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup.xml new file mode 100644 index 0000000000000..bad564c6c9387 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup"> + <annotations> + <description>Update customer website in customer information page</description> + </annotations> + <arguments> + <argument name="websiteName" defaultValue="{{_defaultWebsite.name}}" type="string"/> + </arguments> + <selectOption selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{websiteName}}" stepKey="changeWebsite"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SelectDropdownCustomerAddressAttributeValueActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SelectDropdownCustomerAddressAttributeValueActionGroup.xml index 5d0fb2e7b5c8d..659f0ea5b2640 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/SelectDropdownCustomerAddressAttributeValueActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/SelectDropdownCustomerAddressAttributeValueActionGroup.xml @@ -16,7 +16,7 @@ <argument name="customerAddressAttribute"/> <argument name="optionValue" type="string"/> </arguments> - + <waitForElementVisible selector="{{AdminEditCustomerAddressesSection.dropDownAttribute(customerAddressAttribute.code)}}" stepKey="waitForSelectOption"/> <selectOption selector="{{AdminEditCustomerAddressesSection.dropDownAttribute(customerAddressAttribute.code)}}" userInput="{{optionValue}}" stepKey="selectOptionValue"/> <click selector="{{AdminEditCustomerAddressesSection.save}}" stepKey="saveAddress"/> <waitForPageLoad stepKey="waitForAddressSaved"/> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCreateCustomerSaveActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCreateCustomerSaveActionGroup.xml new file mode 100644 index 0000000000000..d4ac7edffd47a --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCreateCustomerSaveActionGroup.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCreateCustomerSaveActionGroup"> + <click selector="{{StorefrontCustomerCreateFormSection.createAccountButton}}" stepKey="clickCreateAccountButton"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessageVisible"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="Thank you for registering with " stepKey="verifySuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerSaveActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerSaveActionGroup.xml new file mode 100644 index 0000000000000..88c1d70bb9abb --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontCustomerSaveActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCustomerSaveActionGroup"> + <annotations> + <description>Clicks on customer save button in dashboard, asserts success message</description> + </annotations> + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <click selector="{{StorefrontCustomerAccountInformationSection.saveButton}}" stepKey="saveCustomerOnStoreFront"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessageVisible"/> + <see selector="{{StorefrontMessagesSection.success}}" userInput="You saved the account information." stepKey="verifySuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/ImageData.xml b/app/code/Magento/Customer/Test/Mftf/Data/ImageData.xml new file mode 100644 index 0000000000000..3abd51883434b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Data/ImageData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SmallImage" type="imageFile"> + <data key="file">small.jpg</data> + <data key="name">small</data> + <data key="extension">jpg</data> + </entity> +</entities> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml index b2f96eb539c08..d03e088104807 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml @@ -20,5 +20,7 @@ <element name="yesNoOptionAttribute" type="select" selector="//select[@id='{{var}}']/option[2]" parameterized="true"/> <element name="selectedOption" type="text" selector="//select[@id='{{var}}']/option[@selected='selected']" parameterized="true"/> <element name="attributeLabel" type="text" selector="//span[text()='{{attributeLabel}}']" parameterized="true"/> + <element name="fileAttribute" type="file" selector="//input[@type='file' and @name='{{attributeCode}}']" parameterized="true" timeout="30"/> + <element name="customFileAttributeUploadButton" type="input" selector=".file-uploader-area input[name='{{attributeCode}}'] ~ .file-uploader-button" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml new file mode 100644 index 0000000000000..469e27129d2c5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerAssociatedWebsiteTest.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChangeCustomerAssociatedWebsiteTest"> + <annotations> + <features value="Customer"/> + <title value="Admin should be able to change customer associated website ID"/> + <description value="Admin should be able to change customer associated website ID"/> + <severity value="AVERAGE"/> + <useCaseId value="MC-38913"/> + <testCaseId value="MC-39764"/> + <stories value="Customer Edit"/> + <group value="customer"/> + </annotations> + <before> + <!--Login to admin--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create second website--> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> + <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> + </actionGroup> + <!--Create store group and associate it to second website--> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <!--Create store view and associate it to second store group--> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <!--Create customer--> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <!--Delete customer--> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <!--Delete custom website--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <!--Logout from admin--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!--Open customer edit page--> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <!--Navigate to "Account Information" tab--> + <actionGroup ref="AdminOpenAccountInformationTabFromCustomerEditPageActionGroup" stepKey="openAccountInformationEditPage"/> + <!--Verify that "Main Website" is selected in website selector--> + <seeOptionIsSelected selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{_defaultWebsite.name}}" stepKey="assertThatMainWebsiteIsSelected"/> + <!--Change customer website to "Second Website"--> + <actionGroup ref="AdminUpdateCustomerWebsiteInCustomerInformationPageActionGroup" stepKey="updateCustomerWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <!--Verify that changes are saved successfully--> + <actionGroup ref="AdminSaveCustomerAndAssertSuccessMessage" stepKey="assertThatChangesAreSavedSuccessfully"/> + <!--Open customer edit page--> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage2"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + <!--Navigate to "Account Information" tab--> + <actionGroup ref="AdminOpenAccountInformationTabFromCustomerEditPageActionGroup" stepKey="openAccountInformationEditPage2"/> + <!--Verify that "Second Website" is selected in website selector--> + <seeOptionIsSelected selector="{{AdminCustomerAccountInformationSection.associateToWebsite}}" userInput="{{NewWebSiteData.name}}" stepKey="assertThatSecondWebsiteIsSelected"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 7442a32d58b2d..5b6c4fd23e038 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -15,21 +15,24 @@ <title value="Admin should be able to create a customer"/> <description value="Admin should be able to create a customer"/> <severity value="BLOCKER"/> - <testCaseId value="MAGETWO-72095"/> + <testCaseId value="MC-28500"/> <group value="customer"/> <group value="create"/> </annotations> + <before> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCustomerGrid"> <argument name="indices" value="customer_grid"/> </actionGroup> </before> + <after> + <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearCustomersFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="navigateToCustomers"/> <waitForPageLoad stepKey="waitForLoad1"/> <click selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}" stepKey="clickCreateCustomer"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 8dcf494b3572b..824c898068342 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -21,10 +21,11 @@ </annotations> <before> <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> + <actionGroup ref="AdminLoginActionGroup" after="resetCookieForCart" stepKey="loginAsAdmin"/> </before> <after> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <comment userInput="No need this since it done in before section" stepKey="loginAsAdmin"/> <actionGroup ref="DeleteCustomerFromAdminActionGroup" stepKey="deleteCustomerFromAdmin"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php index a10cfe207b822..37239884aeb4f 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Address/FormPostTest.php @@ -495,7 +495,7 @@ public function testExecute( $this->request->expects($this->once()) ->method('isPost') ->willReturn(true); - $this->request->expects($this->exactly(3)) + $this->request->expects($this->exactly(4)) ->method('getParam') ->willReturnMap([ ['id', null, $addressId], diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php index 8e9cce7390831..0e586d0c424a5 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/SaveTest.php @@ -722,7 +722,7 @@ public function testExecuteWithNewCustomerAndValidationException() ]; $extractedData = [ 'coolness' => false, - 'disable_auto_group_change' => 'false', + 'disable_auto_group_change' => 0, 'dob' => '1996-03-12', ]; @@ -731,10 +731,10 @@ public function testExecuteWithNewCustomerAndValidationException() AttributeMetadataInterface::class )->disableOriginalConstructor() ->getMock(); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->once()) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->once()) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -759,12 +759,12 @@ public function testExecuteWithNewCustomerAndValidationException() $objectMock = $this->getMockBuilder(DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->exactly(2)) + $objectMock->expects($this->once()) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->exactly(2)) + $this->objectFactoryMock->expects($this->once()) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -773,19 +773,19 @@ public function testExecuteWithNewCustomerAndValidationException() Form::class )->disableOriginalConstructor() ->getMock(); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->exactly(2)) + $this->formFactoryMock->expects($this->once()) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -809,7 +809,10 @@ public function testExecuteWithNewCustomerAndValidationException() ->method('createAccount') ->with($customerMock, null, '') ->willThrowException(new \Magento\Framework\Validator\Exception(__('Validator Exception'))); - + $this->customerMapperMock->expects($this->once()) + ->method('toFlatArray') + ->with($customerMock) + ->willReturn($extractedData); $customerMock->expects($this->never()) ->method('getId'); @@ -876,7 +879,7 @@ public function testExecuteWithNewCustomerAndLocalizedException() ]; $extractedData = [ 'coolness' => false, - 'disable_auto_group_change' => 'false', + 'disable_auto_group_change' => 0, 'dob' => '1996-03-12', ]; @@ -885,10 +888,10 @@ public function testExecuteWithNewCustomerAndLocalizedException() AttributeMetadataInterface::class )->disableOriginalConstructor() ->getMock(); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->once()) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->once()) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -913,12 +916,12 @@ public function testExecuteWithNewCustomerAndLocalizedException() $objectMock = $this->getMockBuilder(DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->exactly(2)) + $objectMock->expects($this->once()) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->exactly(2)) + $this->objectFactoryMock->expects($this->once()) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -928,19 +931,19 @@ public function testExecuteWithNewCustomerAndLocalizedException() Form::class )->disableOriginalConstructor() ->getMock(); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->exactly(2)) + $this->formFactoryMock->expects($this->once()) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -986,6 +989,11 @@ public function testExecuteWithNewCustomerAndLocalizedException() ->method('addMessage') ->with(new Error('Localized Exception')); + $this->customerMapperMock->expects($this->once()) + ->method('toFlatArray') + ->with($customerMock) + ->willReturn($extractedData); + $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') ->with( @@ -1030,7 +1038,7 @@ public function testExecuteWithNewCustomerAndException() ]; $extractedData = [ 'coolness' => false, - 'disable_auto_group_change' => 'false', + 'disable_auto_group_change' => 0, 'dob' => '1996-03-12', ]; @@ -1039,10 +1047,10 @@ public function testExecuteWithNewCustomerAndException() AttributeMetadataInterface::class )->disableOriginalConstructor() ->getMock(); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->once()) ->method('getAttributeCode') ->willReturn('coolness'); - $attributeMock->expects($this->exactly(2)) + $attributeMock->expects($this->once()) ->method('getFrontendInput') ->willReturn('int'); $attributes = [$attributeMock]; @@ -1067,12 +1075,12 @@ public function testExecuteWithNewCustomerAndException() $objectMock = $this->getMockBuilder(DataObject::class) ->disableOriginalConstructor() ->getMock(); - $objectMock->expects($this->exactly(2)) + $objectMock->expects($this->once()) ->method('getData') ->with('customer') ->willReturn($postValue['customer']); - $this->objectFactoryMock->expects($this->exactly(2)) + $this->objectFactoryMock->expects($this->once()) ->method('create') ->with(['data' => $postValue]) ->willReturn($objectMock); @@ -1081,19 +1089,19 @@ public function testExecuteWithNewCustomerAndException() Form::class )->disableOriginalConstructor() ->getMock(); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('extractData') ->with($this->requestMock, 'customer') ->willReturn($extractedData); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('compactData') ->with($extractedData) ->willReturn($extractedData); - $customerFormMock->expects($this->exactly(2)) + $customerFormMock->expects($this->once()) ->method('getAttributes') ->willReturn($attributes); - $this->formFactoryMock->expects($this->exactly(2)) + $this->formFactoryMock->expects($this->once()) ->method('create') ->with( CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, @@ -1141,6 +1149,11 @@ public function testExecuteWithNewCustomerAndException() ->method('addExceptionMessage') ->with($exception, __('Something went wrong while saving the customer.')); + $this->customerMapperMock->expects($this->once()) + ->method('toFlatArray') + ->with($customerMock) + ->willReturn($extractedData); + $this->sessionMock->expects($this->once()) ->method('setCustomerFormData') ->with( diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php index e1c771d79694e..7a0522f6476f2 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php @@ -313,10 +313,7 @@ public function testMoveTemporaryFileUnableToCreateDirectory() $destinationPath = 'customer/f/i'; - $this->mediaDirectory->expects($this->once()) - ->method('create') - ->with($destinationPath) - ->willReturn(false); + $this->configureMediaDirectoryMock($destinationPath, false); $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); $model->moveTemporaryFile($filePath); @@ -331,10 +328,7 @@ public function testMoveTemporaryFileDestinationFolderDoesNotExists() $destinationPath = 'customer/f/i'; - $this->mediaDirectory->expects($this->once()) - ->method('create') - ->with($destinationPath) - ->willReturn(true); + $this->configureMediaDirectoryMock($destinationPath, true); $this->mediaDirectory->expects($this->once()) ->method('isWritable') ->with($destinationPath) @@ -350,10 +344,7 @@ public function testMoveTemporaryFile() $destinationPath = 'customer/f/i'; - $this->mediaDirectory->expects($this->once()) - ->method('create') - ->with($destinationPath) - ->willReturn(true); + $this->configureMediaDirectoryMock($destinationPath, true); $this->mediaDirectory->expects($this->once()) ->method('isWritable') ->with($destinationPath) @@ -390,10 +381,7 @@ public function testMoveTemporaryFileNewFileName() $destinationPath = 'customer/f/i'; - $this->mediaDirectory->expects($this->once()) - ->method('create') - ->with($destinationPath) - ->willReturn(true); + $this->configureMediaDirectoryMock($destinationPath, true); $this->mediaDirectory->expects($this->once()) ->method('isWritable') ->with($destinationPath) @@ -440,10 +428,7 @@ public function testMoveTemporaryFileWithException() $destinationPath = 'customer/f/i'; - $this->mediaDirectory->expects($this->once()) - ->method('create') - ->with($destinationPath) - ->willReturn(true); + $this->configureMediaDirectoryMock($destinationPath, true); $this->mediaDirectory->expects($this->once()) ->method('isWritable') ->with($destinationPath) @@ -486,4 +471,26 @@ public function testGetMimeType() $this->assertEquals($expected, $model->getMimeType($fileName)); } + + /** + * Configure media directory mock to create media directory. + * + * @param string $destinationPath + * @param bool $directoryCreated + */ + private function configureMediaDirectoryMock(string $destinationPath, bool $directoryCreated): void + { + $this->mediaDirectory->expects($this->at(0)) + ->method('isExist') + ->with('customer/tmp/filename.ext1') + ->willReturn(true); + $this->mediaDirectory->expects($this->at(1)) + ->method('isExist') + ->with('customer/filename.ext1') + ->willReturn(false); + $this->mediaDirectory->expects($this->once()) + ->method('create') + ->with($destinationPath) + ->willReturn($directoryCreated); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php index d3a6e797c7d5c..3c5016df230f9 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/FileTest.php @@ -103,29 +103,31 @@ public function testExtractValueNoRequestScope($expected, $attributeCode = '', $ $value = 'value'; $this->requestMock->expects( - $this->any() + $this->at(0) )->method( 'getParam' - )->willReturn( - ['delete' => $delete] + )->will( + $this->returnValue(['delete' => $delete]) ); $this->attributeMetadataMock->expects( $this->any() )->method( 'getAttributeCode' - )->willReturn( - $attributeCode + )->will( + $this->returnValue($attributeCode) ); if (!empty($attributeCode)) { $_FILES[$attributeCode] = ['attributeCodeValue']; } - $model = $this->initialize([ - 'value' => $value, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertEquals($expected, $model->extractValue($this->requestMock)); if (!empty($attributeCode)) { @@ -157,33 +159,35 @@ public function testExtractValueWithRequestScope($expected, $requestScope, $main $value = 'value'; $this->requestMock->expects( - $this->any() + $this->at(0) )->method( 'getParam' - )->willReturn( - ['delete' => true] + )->will( + $this->returnValue(['delete' => true]) ); $this->requestMock->expects( $this->any() )->method( 'getParams' - )->willReturn( - ['delete' => true] + )->will( + $this->returnValue(['delete' => true]) ); $this->attributeMetadataMock->expects( $this->any() )->method( 'getAttributeCode' - )->willReturn( - 'attributeCode' + )->will( + $this->returnValue('attributeCode') ); - $model = $this->initialize([ - 'value' => $value, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $model->setRequestScope($requestScope); @@ -229,22 +233,24 @@ public function testValidateValueNotToUpload($expected, $value, $isAjax = false, $this->any() )->method( 'isRequired' - )->willReturn( - $isRequired + )->will( + $this->returnValue($isRequired) ); $this->attributeMetadataMock->expects( $this->any() )->method( 'getStoreLabel' - )->willReturn( - 'attributeLabel' + )->will( + $this->returnValue('attributeLabel') ); - $model = $this->initialize([ - 'value' => $value, - 'isAjax' => $isAjax, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $value, + 'isAjax' => $isAjax, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertEquals($expected, $model->validateValue($value)); } @@ -272,39 +278,41 @@ public function testValidateValueToUpload($expected, $value, $parameters = []) { $parameters = array_merge(['uploaded' => true, 'valid' => true], $parameters); - $this->attributeMetadataMock->expects($this->any())->method('isRequired')->willReturn(false); + $this->attributeMetadataMock->expects($this->any())->method('isRequired')->will($this->returnValue(false)); $this->attributeMetadataMock->expects( $this->any() )->method( 'getStoreLabel' - )->willReturn( - 'File Input Field Label' + )->will( + $this->returnValue('File Input Field Label') ); $this->fileValidatorMock->expects( $this->any() )->method( 'getMessages' - )->willReturn( - ['Validation error message.'] + )->will( + $this->returnValue(['Validation error message.']) ); $this->fileValidatorMock->expects( $this->any() )->method( 'isValid' - )->willReturn( - $parameters['valid'] + )->will( + $this->returnValue($parameters['valid']) ); $this->fileProcessorMock->expects($this->any()) ->method('isExist') ->willReturn($parameters['uploaded']); - $model = $this->initialize([ - 'value' => $value, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertEquals($expected, $model->validateValue($value)); } @@ -331,24 +339,28 @@ public function validateValueToUploadDataProvider() public function testCompactValueIsAjax() { - $model = $this->initialize([ - 'value' => 'value', - 'isAjax' => true, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => 'value', + 'isAjax' => true, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertSame($model, $model->compactValue('aValue')); } public function testCompactValueNoDelete() { - $this->attributeMetadataMock->expects($this->any())->method('isRequired')->willReturn(false); + $this->attributeMetadataMock->expects($this->any())->method('isRequired')->will($this->returnValue(false)); - $model = $this->initialize([ - 'value' => 'value', - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => 'value', + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->fileProcessorMock->expects($this->once()) ->method('removeUploadedFile') @@ -360,10 +372,10 @@ public function testCompactValueNoDelete() public function testCompactValueDelete() { - $this->attributeMetadataMock->expects($this->any())->method('isRequired')->willReturn(false); + $this->attributeMetadataMock->expects($this->any())->method('isRequired')->will($this->returnValue(false)); $mediaDirMock = $this->getMockForAbstractClass( - WriteInterface::class + \Magento\Framework\Filesystem\Directory\WriteInterface::class ); $mediaDirMock->expects($this->once()) ->method('delete') @@ -372,13 +384,15 @@ public function testCompactValueDelete() $this->fileSystemMock->expects($this->once()) ->method('getDirectoryWrite') ->with(DirectoryList::MEDIA) - ->willReturn($mediaDirMock); + ->will($this->returnValue($mediaDirMock)); - $model = $this->initialize([ - 'value' => 'value', - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => 'value', + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertSame('', $model->compactValue(['delete' => true])); } @@ -389,20 +403,20 @@ public function testCompactValueTmpFile() $expected = 'saved.file'; $mediaDirMock = $this->getMockForAbstractClass( - WriteInterface::class + \Magento\Framework\Filesystem\Directory\WriteInterface::class ); $this->fileSystemMock->expects($this->once()) ->method('getDirectoryWrite') ->with(DirectoryList::MEDIA) - ->willReturn($mediaDirMock); + ->will($this->returnValue($mediaDirMock)); $mediaDirMock->expects($this->any()) ->method('getAbsolutePath') - ->willReturnArgument(0); - $uploaderMock = $this->createMock(Uploader::class); + ->will($this->returnArgument(0)); + $uploaderMock = $this->createMock(\Magento\Framework\File\Uploader::class); $this->uploaderFactoryMock->expects($this->once()) ->method('create') ->with(['fileId' => $value]) - ->willReturn($uploaderMock); + ->will($this->returnValue($uploaderMock)); $uploaderMock->expects($this->once()) ->method('setFilesDispersion') ->with(true); @@ -417,13 +431,15 @@ public function testCompactValueTmpFile() ->with(self::ENTITY_TYPE, 'new.file'); $uploaderMock->expects($this->once()) ->method('getUploadedFileName') - ->willReturn($expected); + ->will($this->returnValue($expected)); - $model = $this->initialize([ - 'value' => null, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => null, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertSame($expected, $model->compactValue($value)); } @@ -432,11 +448,13 @@ public function testRestoreValue() { $value = 'value'; - $model = $this->initialize([ - 'value' => $value, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertEquals($value, $model->restoreValue('aValue')); } @@ -447,11 +465,13 @@ public function testRestoreValue() */ public function testOutputValueNonJson($format) { - $model = $this->initialize([ - 'value' => 'value', - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => 'value', + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertSame('', $model->outputValue($format)); } @@ -480,29 +500,31 @@ public function testOutputValueJson() )->method( 'encode' )->with( - $value - )->willReturn( - $urlKey + $this->equalTo($value) + )->will( + $this->returnValue($urlKey) ); $expected = ['value' => $value, 'url_key' => $urlKey]; - $model = $this->initialize([ - 'value' => $value, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertSame($expected, $model->outputValue(ElementFactory::OUTPUT_FORMAT_JSON)); } /** * @param array $data - * @return File + * @return \Magento\Customer\Model\Metadata\Form\File */ private function initialize(array $data) { - return new File( + return new \Magento\Customer\Model\Metadata\Form\File( $this->localeMock, $this->loggerMock, $this->attributeMetadataMock, @@ -528,22 +550,26 @@ public function testExtractValueFileUploaderUIComponent() ->method('getAttributeCode') ->willReturn($attributeCode); - $this->requestMock->expects($this->once()) + $this->requestMock->expects($this->at(0)) ->method('getParam') ->with($requestScope) - ->willReturn([ - $attributeCode => [ - [ - 'file' => $fileName, + ->willReturn( + [ + $attributeCode => [ + [ + 'file' => $fileName, + ], ], - ], - ]); - - $model = $this->initialize([ - 'value' => 'value', - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + ] + ); + + $model = $this->initialize( + [ + 'value' => 'value', + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $model->setRequestScope($requestScope); $result = $model->extractValue($this->requestMock); @@ -555,11 +581,13 @@ public function testCompactValueRemoveUiComponentValue() { $value = 'value'; - $model = $this->initialize([ - 'value' => $value, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->fileProcessorMock->expects($this->once()) ->method('removeUploadedFile') @@ -573,11 +601,13 @@ public function testCompactValueNoAction() { $value = 'value'; - $model = $this->initialize([ - 'value' => $value, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $value, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertEquals($value, $model->compactValue($value)); } @@ -588,11 +618,13 @@ public function testCompactValueUiComponent() 'file' => 'filename', ]; - $model = $this->initialize([ - 'value' => null, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => null, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->fileProcessorMock->expects($this->once()) ->method('moveTemporaryFile') @@ -613,7 +645,7 @@ public function testCompactValueInputField() $uploadedFilename = 'filename.ext1'; $mediaDirectoryMock = $this->getMockBuilder( - WriteInterface::class + \Magento\Framework\Filesystem\Directory\WriteInterface::class ) ->getMockForAbstractClass(); $mediaDirectoryMock->expects($this->once()) @@ -627,9 +659,8 @@ public function testCompactValueInputField() ->willReturn($mediaDirectoryMock); $uploaderMock = $this->getMockBuilder( - Uploader::class - )->disableOriginalConstructor() - ->getMock(); + \Magento\Framework\File\Uploader::class + )->disableOriginalConstructor()->getMock(); $uploaderMock->expects($this->once()) ->method('setFilesDispersion') ->with(true) @@ -655,11 +686,13 @@ public function testCompactValueInputField() ->with(['fileId' => $value]) ->willReturn($uploaderMock); - $model = $this->initialize([ - 'value' => null, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => null, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertEquals($uploadedFilename, $model->compactValue($value)); } @@ -674,7 +707,7 @@ public function testCompactValueInputFieldWithException() $originValue = 'origin'; $mediaDirectoryMock = $this->getMockBuilder( - WriteInterface::class + \Magento\Framework\Filesystem\Directory\WriteInterface::class )->getMockForAbstractClass(); $mediaDirectoryMock->expects($this->once()) ->method('delete') @@ -697,11 +730,13 @@ public function testCompactValueInputFieldWithException() ->with($exception) ->willReturnSelf(); - $model = $this->initialize([ - 'value' => $originValue, - 'isAjax' => false, - 'entityTypeCode' => self::ENTITY_TYPE, - ]); + $model = $this->initialize( + [ + 'value' => $originValue, + 'isAjax' => false, + 'entityTypeCode' => self::ENTITY_TYPE, + ] + ); $this->assertEquals('', $model->compactValue($value)); } diff --git a/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php index f72cbbc281e90..146cecb09351f 100644 --- a/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php +++ b/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php @@ -341,7 +341,7 @@ public function testAfterAddressSaveDefaultGroup( $customer->expects($this->once()) ->method('getDisableAutoGroupChange') ->willReturn(false); - $customer->expects($this->once()) + $customer->expects($this->exactly(2)) ->method('getGroupId') ->willReturn(null); $customer->expects($this->once()) diff --git a/app/code/Magento/CustomerGraphQl/Model/Context/AddCustomerGroupToContext.php b/app/code/Magento/CustomerGraphQl/Model/Context/AddCustomerGroupToContext.php new file mode 100644 index 0000000000000..aaa2b85636f79 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Context/AddCustomerGroupToContext.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Context; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\GraphQl\Model\Query\ContextParametersInterface; +use Magento\GraphQl\Model\Query\ContextParametersProcessorInterface; +use Magento\Customer\Model\Group; +use Magento\Customer\Model\Group\Resolver as CustomerGroupResolver; + +/** + * @inheritdoc + */ +class AddCustomerGroupToContext implements ContextParametersProcessorInterface +{ + /** + * @var CustomerGroupResolver + */ + private $customerGroupResolver; + + /** + * @param CustomerGroupResolver $customerGroupResolver + */ + public function __construct( + CustomerGroupResolver $customerGroupResolver + ) { + $this->customerGroupResolver = $customerGroupResolver; + } + + /** + * @inheritdoc + */ + public function execute(ContextParametersInterface $contextParameters): ContextParametersInterface + { + $customerGroupId = null; + $extensionAttributes = $contextParameters->getExtensionAttributesData(); + if ($contextParameters->getUserType() === UserContextInterface::USER_TYPE_GUEST) { + $customerGroupId = Group::NOT_LOGGED_IN_ID; + } elseif (!empty($extensionAttributes) && $extensionAttributes['is_customer'] === true) { + $customerGroupId = $this->customerGroupResolver->resolve((int) $contextParameters->getUserId()); + } + if ($customerGroupId !== null) { + $contextParameters->addExtensionAttribute('customer_group_id', (int) $customerGroupId); + } + return $contextParameters; + } +} diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index 3ed77a2ad563c..3e3a5327370a5 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -17,6 +17,7 @@ <arguments> <argument name="contextParametersProcessors" xsi:type="array"> <item name="add_user_info_to_context" xsi:type="object">Magento\CustomerGraphQl\Model\Context\AddUserInfoToContext</item> + <item name="add_customer_group_to_context" xsi:type="object">Magento\CustomerGraphQl\Model\Context\AddCustomerGroupToContext</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 9781b521597ba..513e15c2a4dc4 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -1756,7 +1756,7 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') foreach ($package['items'] as $item) { $content[] = $item['name']; } - $nodePiece->addChild('PieceContents', substr(implode(',', $content), 0, 34)); + $nodePiece->addChild('PieceContents', $this->string->substr(implode(',', $content), 0, 34)); } $nodeShipmentDetails->addChild('Weight', sprintf('%.3f', $rawRequest->getPackageWeight())); @@ -1776,7 +1776,7 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') $nodeShipmentDetails->addChild('DoorTo', 'DD'); $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); $contentType = isset($package['params']['container']) ? $package['params']['container'] : ''; - $packageType = $contentType === self::DHL_CONTENT_TYPE_NON_DOC ? 'CP' : ''; + $packageType = $contentType === self::DHL_CONTENT_TYPE_NON_DOC ? 'CP' : 'EE'; $nodeShipmentDetails->addChild('PackageType', $packageType); if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { $nodeShipmentDetails->addChild('IsDutiable', 'Y'); diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls index 6daf13f567d4b..06d7d4817ee02 100644 --- a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls @@ -24,7 +24,7 @@ type ExchangeRate { } type Country { - id: String + id: String @doc(description: "The unique ID for a `Country` object.") two_letter_abbreviation: String three_letter_abbreviation: String full_name_locale: String @@ -33,7 +33,7 @@ type Country { } type Region { - id: Int + id: Int @doc(description: "The unique ID for a `Region` object.") code: String name: String } diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml index b89aa7d126686..2ced91731e4ba 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml @@ -30,9 +30,8 @@ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="createDownloadableProduct"/> </createData> - - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'indexer:reindex' command for preserving Backward Compatibility" stepKey="reindex"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signIn"> @@ -51,8 +50,8 @@ <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'indexer:reindex' command for preserving Backward Compatibility" stepKey="reindex"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> </after> <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" stepKey="addSimpleProductToCart"> diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 8248343fcb120..d8e9c9615b618 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -53,7 +53,7 @@ type DownloadableProductLinks @doc(description: "DownloadableProductLinks define link_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_file: String @deprecated(reason: "`sample_url` serves to get the downloadable sample") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") + uid: ID! @doc(description: "The unique ID for a `DownloadableProductLinks` object.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") } type DownloadableProductSamples @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") { @@ -80,7 +80,7 @@ type DownloadableCreditMemoItem implements CreditMemoItemInterface { type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") + uid: ID! @doc(description: "The unique ID for a `DownloadableItemsLinks` object.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") } type DownloadableWishlistItem implements WishlistItemInterface @doc(description: "A downloadable product wish list item") { diff --git a/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php b/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php new file mode 100644 index 0000000000000..fdc71faa90902 --- /dev/null +++ b/app/code/Magento/Eav/Model/Mview/ChangeLogBatchWalker.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Eav\Model\Mview; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Sql\Expression; +use Magento\Framework\Mview\View\ChangeLogBatchWalkerInterface; +use Magento\Framework\Mview\View\ChangelogInterface; + +/** + * Class BatchIterator + */ +class ChangeLogBatchWalker implements ChangeLogBatchWalkerInterface +{ + private const GROUP_CONCAT_MAX_VARIABLE = 'group_concat_max_len'; + /** ID is defined as small int. Default size of it is 5 */ + private const DEFAULT_ID_SIZE = 5; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var array + */ + private $entityTypeCodes; + + /** + * @param ResourceConnection $resourceConnection + * @param array $entityTypeCodes + */ + public function __construct( + ResourceConnection $resourceConnection, + array $entityTypeCodes = [] + ) { + $this->resourceConnection = $resourceConnection; + $this->entityTypeCodes = $entityTypeCodes; + } + + /** + * Calculate EAV attributes size + * + * @param ChangelogInterface $changelog + * @return int + * @throws \Exception + */ + private function calculateEavAttributeSize(ChangelogInterface $changelog): int + { + $connection = $this->resourceConnection->getConnection(); + + if (!isset($this->entityTypeCodes[$changelog->getViewId()])) { + throw new \Exception('Entity type for view was not defined'); + } + + $select = $connection->select(); + $select->from( + $this->resourceConnection->getTableName('eav_attribute'), + new Expression('COUNT(*)') + ) + ->joinInner( + ['type' => $connection->getTableName('eav_entity_type')], + 'type.entity_type_id=eav_attribute.entity_type_id' + ) + ->where('type.entity_type_code = ?', $this->entityTypeCodes[$changelog->getViewId()]); + + return (int) $connection->fetchOne($select); + } + + /** + * Prepare group max concat + * + * @param int $numberOfAttributes + * @return void + * @throws \Exception + */ + private function setGroupConcatMax(int $numberOfAttributes): void + { + $connection = $this->resourceConnection->getConnection(); + $connection->query(sprintf( + 'SET SESSION %s=%s', + self::GROUP_CONCAT_MAX_VARIABLE, + $numberOfAttributes * (self::DEFAULT_ID_SIZE + 1) + )); + } + + /** + * @inheritdoc + * @throws \Exception + */ + public function walk(ChangelogInterface $changelog, int $fromVersionId, int $toVersion, int $batchSize) + { + $connection = $this->resourceConnection->getConnection(); + $numberOfAttributes = $this->calculateEavAttributeSize($changelog); + $this->setGroupConcatMax($numberOfAttributes); + $select = $connection->select()->distinct(true) + ->where( + 'version_id > ?', + (int) $fromVersionId + ) + ->where( + 'version_id <= ?', + $toVersion + ) + ->group([$changelog->getColumnName(), 'store_id']) + ->limit($batchSize); + + $columns = [ + $changelog->getColumnName(), + 'attribute_ids' => new Expression('GROUP_CONCAT(attribute_id)'), + 'store_id' + ]; + $select->from($changelog->getName(), $columns); + return $connection->fetchAll($select); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/events.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/events.xml similarity index 60% rename from app/code/Magento/CatalogUrlRewrite/etc/adminhtml/events.xml rename to app/code/Magento/GiftMessageGraphQl/etc/graphql/events.xml index 9c4a8aaf41231..2411221ded375 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/events.xml +++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/events.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> - <event name="catalog_product_to_website_change"> - <observer name="catalog_product_to_website_change" instance="Magento\CatalogUrlRewrite\Observer\ProductToWebsiteChangeObserver"/> + <event name="sales_model_service_quote_submit_before"> + <observer name="giftmessage" instance="Magento\GiftMessage\Observer\SalesEventQuoteSubmitBeforeObserver" shared="false" /> </event> </config> diff --git a/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml b/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml index d700b0e9e7668..6d8d42a5d3f8f 100644 --- a/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml +++ b/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml @@ -14,6 +14,11 @@ <value id="google_analytics" type="host">www.google-analytics.com</value> </values> </policy> + <policy id="connect-src"> + <values> + <value id="google_analytics" type="host">www.google-analytics.com</value> + </values> + </policy> <policy id="img-src"> <values> <value id="google_ad_services" type="host">www.googleadservices.com</value> diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index 7195c05c0877b..865e8f223db54 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -14,6 +14,7 @@ <preference for="Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface" type="Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeReaderComposite"/> <preference for="Magento\GraphQl\Model\Query\ContextFactoryInterface" type="Magento\GraphQl\Model\Query\ContextFactory"/> <preference for="Magento\GraphQl\Model\Query\ContextParametersInterface" type="Magento\GraphQl\Model\Query\ContextParameters"/> + <preference for="Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface" type="Magento\Framework\GraphQl\Query\Resolver\ArgumentsCompositeProcessor"/> <type name="Magento\Framework\App\AreaList"> <arguments> <argument name="areas" xsi:type="array"> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 7366567c2b95d..65280ad1ad2aa 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -279,6 +279,6 @@ enum CurrencyEnum @doc(description: "The list of available currency codes") { } input EnteredOptionInput @doc(description: "Defines a customer-entered option") { - uid: ID! @doc(description: "An encoded ID") + uid: ID! @doc(description: "The unique ID for a `CustomizableFieldOption`, `CustomizableFileOption`, `CustomizableAreaOption`, etc. of `CustomizableOptionInterface` objects") value: String! @doc(description: "Text the customer entered") } diff --git a/app/code/Magento/GroupedProduct/Api/Data/GroupedOptionsInterface.php b/app/code/Magento/GroupedProduct/Api/Data/GroupedOptionsInterface.php new file mode 100644 index 0000000000000..9278ef168eb0d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Api/Data/GroupedOptionsInterface.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; + +/** + * Represents `product item id with qty` of a grouped product. + */ +interface GroupedOptionsInterface extends ExtensibleDataInterface +{ + /** + * Get associated product id + * + * @return int|null + */ + public function getId(): ?int; + + /** + * Get associated product qty + * + * @return int|null + */ + public function getQty(): ?int; + + /** + * Set extension attributes + * + * @param \Magento\GroupedProduct\Api\Data\GroupedOptionsExtensionInterface $extensionAttributes + * @return void + */ + public function setExtensionAttributes(GroupedOptionsExtensionInterface $extensionAttributes): void; + + /** + * Get extension attributes + * + * @return \Magento\GroupedProduct\Api\Data\GroupedOptionsExtensionInterface|null + */ + public function getExtensionAttributes(): ?GroupedOptionsExtensionInterface; +} diff --git a/app/code/Magento/GroupedProduct/Model/Quote/Item/CartItemProcessor.php b/app/code/Magento/GroupedProduct/Model/Quote/Item/CartItemProcessor.php new file mode 100644 index 0000000000000..78ab91194992e --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Quote/Item/CartItemProcessor.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Quote\Item; + +use Magento\Framework\DataObject; +use Magento\Framework\DataObject\Factory as ObjectFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\GroupedProduct\Api\Data\GroupedOptionsInterface; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\Quote\Api\Data as QuoteApi; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Model\Quote\Item\CartItemProcessorInterface; + +/** + * Converts grouped_options to super_group for the grouped product. + */ +class CartItemProcessor implements CartItemProcessorInterface +{ + private const SUPER_GROUP_CODE = 'super_group'; + + /** + * @var ObjectFactory + */ + private $objectFactory; + + /** + * @var QuoteApi\ProductOptionExtensionFactory + */ + private $productOptionExtensionFactory; + + /** + * @var QuoteApi\ProductOptionInterfaceFactory + */ + private $productOptionFactory; + + /** + * @var array|null + */ + private $groupedOptions; + + /** + * @param ObjectFactory $objectFactory + * @param QuoteApi\ProductOptionExtensionFactory $productOptionExtensionFactory + * @param QuoteApi\ProductOptionInterfaceFactory $productOptionFactory + */ + public function __construct( + ObjectFactory $objectFactory, + QuoteApi\ProductOptionExtensionFactory $productOptionExtensionFactory, + QuoteApi\ProductOptionInterfaceFactory $productOptionFactory + ) { + $this->objectFactory = $objectFactory; + $this->productOptionExtensionFactory = $productOptionExtensionFactory; + $this->productOptionFactory = $productOptionFactory; + } + + /** + * Converts the grouped_options request data into the same format as native frontend add-to-cart + * + * @param CartItemInterface $cartItem + * @return DataObject|null + */ + public function convertToBuyRequest(CartItemInterface $cartItem): ?DataObject + { + if ($cartItem->getProductOption() + && $cartItem->getProductOption()->getExtensionAttributes() + && $cartItem->getProductOption()->getExtensionAttributes()->getGroupedOptions() + ) { + $groupedOptions = $cartItem->getProductOption()->getExtensionAttributes()->getGroupedOptions(); + $this->groupedOptions = $groupedOptions; + + return $this->objectFactory->create($this->getConvertedData($groupedOptions)); + } + + return null; + } + + /** + * Returns grouped_options converted to super_group data + * + * @param GroupedOptionsInterface[] $groupedOptions + * @return array + * @throws LocalizedException + */ + private function getConvertedData(array $groupedOptions): array + { + $requestData = []; + foreach ($groupedOptions as $item) { + /** @var GroupedOptionsInterface $item */ + if ($item->getQty() === null || $item->getId() === null) { + throw new LocalizedException(__('Please specify id and qty for grouped options.')); + } + $requestData[self::SUPER_GROUP_CODE][$item->getId()] = $item->getQty(); + } + + return $requestData; + } + + /** + * Option processor + * + * @param CartItemInterface $cartItem + * @return CartItemInterface + */ + public function processOptions(CartItemInterface $cartItem): CartItemInterface + { + if (empty($this->groupedOptions) || $cartItem->getProductType() !== Grouped::TYPE_CODE) { + return $cartItem; + } + + $extension = $this->productOptionExtensionFactory->create() + ->setGroupedOptions($this->groupedOptions); + if (!$cartItem->getProductOption()) { + $cartItem->setProductOption($this->productOptionFactory->create()); + } + $cartItem->getProductOption()->setExtensionAttributes($extension); + + return $cartItem; + } +} diff --git a/app/code/Magento/GroupedProduct/Model/Quote/Item/GroupedOptions.php b/app/code/Magento/GroupedProduct/Model/Quote/Item/GroupedOptions.php new file mode 100644 index 0000000000000..3b249a7b5d548 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Quote/Item/GroupedOptions.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Quote\Item; + +use Magento\GroupedProduct\Api\Data\GroupedOptionsInterface; +use Magento\GroupedProduct\Api\Data\GroupedOptionsExtensionInterface; + +/** + * @inheritDoc + */ +class GroupedOptions implements GroupedOptionsInterface +{ + /** + * @var int|null + */ + private $qty; + + /** + * @var int|null + */ + private $id; + + /** + * @var GroupedOptionsExtensionInterface|null + */ + private $extensionAttributes; + + /** + * @param int|null $id + * @param int|null $qty + * @param GroupedOptionsExtensionInterface|null $extensionAttributes + */ + public function __construct( + ?int $id = null, + ?int $qty = null, + ?GroupedOptionsExtensionInterface $extensionAttributes = null + ) { + $this->id = $id; + $this->qty = $qty; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritDoc + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * @inheritDoc + */ + public function getQty(): ?int + { + return $this->qty; + } + + /** + * @inheritDoc + */ + public function setExtensionAttributes(GroupedOptionsExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritDoc + */ + public function getExtensionAttributes(): ?GroupedOptionsExtensionInterface + { + return $this->extensionAttributes; + } +} diff --git a/app/code/Magento/GroupedProduct/Model/ResourceModel/Indexer/Stock/Grouped.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Indexer/Stock/Grouped.php index 9d29b02f68bf1..b2a8a361564e0 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Indexer/Stock/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Indexer/Stock/Grouped.php @@ -94,7 +94,7 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f $select->columns(['status' => $stockStatusExpr]); if ($entityIds !== null) { - $select->where('e.entity_id IN(?)', $entityIds); + $select->where('e.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } return $select; diff --git a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php index c5f0316feb126..4c24cdb752d3c 100644 --- a/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/ResourceModel/Product/Indexer/Price/Grouped.php @@ -166,7 +166,7 @@ private function prepareGroupedProductPriceDataSelect(array $dimensions, array $ ); if ($entityIds !== null) { - $select->where('e.entity_id IN(?)', $entityIds); + $select->where('e.entity_id IN(?)', $entityIds, \Zend_Db::INT_TYPE); } return $select; diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminAssertProductOnGroupedOptionGridActionGroup.xml b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminAssertProductOnGroupedOptionGridActionGroup.xml new file mode 100644 index 0000000000000..2f706f45f834d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/ActionGroup/AdminAssertProductOnGroupedOptionGridActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertProductOnGroupedOptionGridActionGroup"> + <annotations> + <description>Admin assert product on grouped option grid.</description> + </annotations> + <arguments> + <argument name="product"/> + </arguments> + + <grabTextFrom selector="{{AdminGroupedProductOptionGridSection.productName}}" stepKey="grabProductName"/> + <assertEquals stepKey="assertProductName"> + <expectedResult type="string">{{product.name}}</expectedResult> + <actualResult type="variable">$grabProductName</actualResult> + </assertEquals> + + <grabTextFrom selector="{{AdminGroupedProductOptionGridSection.productSku}}" stepKey="grabProductSku"/> + <assertEquals stepKey="assertProductSku"> + <expectedResult type="string">{{product.sku}}</expectedResult> + <actualResult type="variable">$grabProductSku</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminGroupedProductOptionGridSection.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminGroupedProductOptionGridSection.xml new file mode 100644 index 0000000000000..1fff52c1d30cd --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Section/AdminGroupedProductOptionGridSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminGroupedProductOptionGridSection"> + <element name="productName" type="text" selector=".data-row td[data-index='name']"/> + <element name="productSku" type="text" selector=".data-row td[data-index='sku']"/> + </section> +</sections> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml new file mode 100644 index 0000000000000..d5dcd7f48b956 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateGroupedProductNonDefaultAttributeSetTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Create product"/> + <title value="Create Grouped Product when non-default attribute set is chosen"/> + <description value="Create Grouped Product with simple when non-default attribute set is chosen"/> + <testCaseId value="MC-39950"/> + <severity value="MAJOR"/> + <group value="groupedProduct"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="createSimpleProduct"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + </after> + + <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createGroupedProduct"> + <argument name="productType" value="grouped"/> + </actionGroup> + <actionGroup ref="AdminProductPageSelectAttributeSetActionGroup" stepKey="selectAttributeSet"> + <argument name="attributeSetName" value="$createAttributeSet.attribute_set_name$"/> + </actionGroup> + <actionGroup ref="FillGroupedProductFormActionGroup" stepKey="fillProductForm"> + <argument name="product" value="GroupedProduct"/> + </actionGroup> + <actionGroup ref="AdminAssignProductToGroupActionGroup" stepKey="addSimpleToGroup"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + <actionGroup ref="AdminAssertProductOnGroupedOptionGridActionGroup" stepKey="assertProductOptionGrid"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php index 3ea8c6eb3c2b9..12a21a4ebd04b 100644 --- a/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php +++ b/app/code/Magento/GroupedProduct/Ui/DataProvider/Product/Form/Modifier/Grouped.php @@ -562,6 +562,7 @@ protected function fillMeta() 'fit' => true, 'label' => __('Thumbnail'), 'sortOrder' => 20, + 'labelVisible' => false, ], ], ], @@ -586,6 +587,7 @@ protected function fillMeta() 'validation' => [ 'validate-number' => true, ], + 'labelVisible' => false, ], ], ], @@ -601,7 +603,8 @@ protected function fillMeta() 'elementTmpl' => 'Magento_GroupedProduct/components/position', 'sortOrder' => 90, 'fit' => true, - 'dataScope' => 'positionCalculated' + 'dataScope' => 'positionCalculated', + 'labelVisible' => false, ], ], ], @@ -660,6 +663,7 @@ protected function getTextColumn($dataScope, $fit, Phrase $label, $sortOrder) 'fit' => $fit, 'label' => $label, 'sortOrder' => $sortOrder, + 'labelVisible' => false, ], ], ], diff --git a/app/code/Magento/GroupedProduct/etc/di.xml b/app/code/Magento/GroupedProduct/etc/di.xml index 924d2d1fc9669..8b58ac08ebd31 100644 --- a/app/code/Magento/GroupedProduct/etc/di.xml +++ b/app/code/Magento/GroupedProduct/etc/di.xml @@ -6,6 +6,8 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\GroupedProduct\Api\Data\GroupedOptionsInterface" type="Magento\GroupedProduct\Model\Quote\Item\GroupedOptions" /> + <type name="Magento\Quote\Model\Quote\Item\RelatedProducts"> <arguments> <argument name="relatedProductTypes" xsi:type="array"> @@ -105,6 +107,13 @@ </argument> </arguments> </type> + <type name="Magento\Quote\Model\Quote\Item\Repository"> + <arguments> + <argument name="cartItemProcessors" xsi:type="array"> + <item name="grouped" xsi:type="object">Magento\GroupedProduct\Model\Quote\Item\CartItemProcessor\Proxy</item> + </argument> + </arguments> + </type> <type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver"> <arguments> <argument name="parentItemProcessorPool" xsi:type="array"> diff --git a/app/code/Magento/GroupedProduct/etc/extension_attributes.xml b/app/code/Magento/GroupedProduct/etc/extension_attributes.xml index 14ff9821025c4..b614a1277de76 100644 --- a/app/code/Magento/GroupedProduct/etc/extension_attributes.xml +++ b/app/code/Magento/GroupedProduct/etc/extension_attributes.xml @@ -9,4 +9,8 @@ <extension_attributes for="Magento\Catalog\Api\Data\ProductLinkInterface"> <attribute code="qty" type="float" /> </extension_attributes> + + <extension_attributes for="Magento\Quote\Api\Data\ProductOptionInterface"> + <attribute code="grouped_options" type="Magento\GroupedProduct\Api\Data\GroupedOptionsInterface[]" /> + </extension_attributes> </config> diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php index 6da90efa4592c..4092879e23622 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php @@ -9,6 +9,8 @@ use Magento\ImportExport\Model\Import\Entity\AbstractEntity; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\History as ModelHistory; +use Magento\Framework\Escaper; +use Magento\Framework\App\ObjectManager; /** * Import controller @@ -37,22 +39,31 @@ abstract class ImportResult extends Import */ protected $reportHelper; + /** + * @var Escaper|null + */ + protected $escaper; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor * @param \Magento\ImportExport\Model\History $historyModel * @param \Magento\ImportExport\Helper\Report $reportHelper + * @param Escaper|null $escaper */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor, \Magento\ImportExport\Model\History $historyModel, - \Magento\ImportExport\Helper\Report $reportHelper + \Magento\ImportExport\Helper\Report $reportHelper, + Escaper $escaper = null ) { parent::__construct($context); $this->reportProcessor = $reportProcessor; $this->historyModel = $historyModel; $this->reportHelper = $reportHelper; + $this->escaper = $escaper + ?? ObjectManager::getInstance()->get(Escaper::class); } /** @@ -69,28 +80,30 @@ protected function addErrorMessages( if ($errorAggregator->getErrorsCount()) { $message = ''; $counter = 0; + $escapedMessages = []; foreach ($this->getErrorMessages($errorAggregator) as $error) { - $message .= (++$counter) . '. ' . $error . '<br>'; + $escapedMessages[] = (++$counter) . '. ' . $this->escaper->escapeHtml($error); if ($counter >= self::LIMIT_ERRORS_MESSAGE) { break; } } if ($errorAggregator->hasFatalExceptions()) { foreach ($this->getSystemExceptions($errorAggregator) as $error) { - $message .= $error->getErrorMessage() + $escapedMessages[] = $this->escaper->escapeHtml($error->getErrorMessage()) . ' <a href="#" onclick="$(this).next().show();$(this).hide();return false;">' . __('Show more') . '</a><div style="display:none;">' . __('Additional data') . ': ' - . $error->getErrorDescription() . '</div>'; + . $this->escaper->escapeHtml($error->getErrorDescription()) . '</div>'; } } try { + $message .= implode('<br>', $escapedMessages); $resultBlock->addNotice( '<strong>' . __('Following Error(s) has been occurred during importing process:') . '</strong><br>' . '<div class="import-error-wrapper">' . __('Only the first 100 errors are shown. ') . '<a href="' . $this->createDownloadUrlImportHistoryFile($this->createErrorReport($errorAggregator)) . '">' . __('Download full report') . '</a><br>' - . '<div class="import-error-list">' . $resultBlock->escapeHtml($message) . '</div></div>' + . '<div class="import-error-list">' . $message . '</div></div>' ); } catch (\Exception $e) { foreach ($this->getErrorMessages($errorAggregator) as $errorMessage) { diff --git a/app/code/Magento/Indexer/Model/ProcessManager.php b/app/code/Magento/Indexer/Model/ProcessManager.php index 2f2c500e028cf..2b25c8c6a3d15 100644 --- a/app/code/Magento/Indexer/Model/ProcessManager.php +++ b/app/code/Magento/Indexer/Model/ProcessManager.php @@ -7,6 +7,9 @@ namespace Magento\Indexer\Model; +use Magento\Framework\App\ObjectManager; +use Psr\Log\LoggerInterface; + /** * Provide functionality for executing user functions in multi-thread mode. */ @@ -29,15 +32,22 @@ class ProcessManager /** @var int|null */ private $threadsCount; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\Framework\Registry $registry * @param int|null $threadsCount + * @param LoggerInterface|null $logger */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Framework\Registry $registry = null, - int $threadsCount = null + int $threadsCount = null, + LoggerInterface $logger = null ) { $this->resource = $resource; if (null === $registry) { @@ -47,6 +57,9 @@ public function __construct( } $this->registry = $registry; $this->threadsCount = (int)$threadsCount; + $this->logger = $logger ?? ObjectManager::getInstance()->get( + LoggerInterface::class + ); } /** @@ -135,11 +148,20 @@ private function isSetupMode(): bool */ private function startChildProcess(callable $userFunction) { - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $status = call_user_func($userFunction); - $status = is_int($status) ? $status : 0; - // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage - exit($status); + try { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $status = call_user_func($userFunction); + $status = is_int($status) ? $status : 0; + } catch (\Throwable $e) { + $status = 1; + $this->logger->error( + __('Child process failed with message: %1', $e->getMessage()), + ['exception' => $e] + ); + } finally { + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage + exit($status); + } } /** diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml index d474094dcd54b..e7e7ba82bf09c 100644 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminReindexAndFlushCacheActionGroup.xml @@ -9,6 +9,15 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminReindexAndFlushCache"> <annotations> + <!-- + PLEASE NOTE: + The action group runs commands to reindex ALL indexers and to flush ALL cache types. + It's better to specify needed index (for reindexing) / cache type (for cache flushing). + Please use the following action groups: + - CliIndexerReindexActionGroup - run reindex by CLI with specified indexers + - CliCacheCleanActionGroup - run cache:clean by CLI with specified cache tags + - CliCacheFlushActionGroup - run cache:flush by CLI with specified cache tags + --> <description>Run reindex and flush cache.</description> </annotations> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml index c083383dd8861..f5919c6ccbb80 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> @@ -38,7 +38,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Admin Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml index 1175103395427..09e48b5c61aaf 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml @@ -28,7 +28,7 @@ <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="enableAddStoreCodeToUrls"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> </before> @@ -43,7 +43,7 @@ <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="disableAddStoreCodeToUrls"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml index f9418a9cf1e1b..3b2f61339b921 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml @@ -26,7 +26,7 @@ <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="enableAddStoreCodeToUrls"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createCustomWebsite"> <argument name="newWebsiteName" value="{{customWebsite.name}}"/> @@ -60,7 +60,7 @@ <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="disableAddStoreCodeToUrls"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml index cf90f0b6a8511..3a80bbb7a6f2e 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAdmin"/> </before> @@ -32,7 +32,7 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer Login from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml index 4ef72d949065d..e4a6767ce7b22 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml @@ -19,7 +19,7 @@ </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml index 5b5e9e21113c8..6ae6ddfeccb47 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml @@ -24,7 +24,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="NewAdminUser" stepKey="createNewAdmin"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createFirstCustomer"/> <createData entity="Simple_US_CA_Customer_Assistance_Allowed" stepKey="createSecondCustomer"/> @@ -40,7 +40,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login into First Customer account --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml index e4f0209c55233..8493fda17636a 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml @@ -29,7 +29,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 1" stepKey="enableLoginAsCustomerManualChoose"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createCustomStore"> <argument name="website" value="{{_defaultWebsite.name}}"/> @@ -54,7 +54,7 @@ stepKey="disableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml index 5f706a814eb71..551139fb8095e 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml @@ -25,7 +25,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 1" stepKey="enableLoginAsCustomerManualChoose"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createCustomStore"> <argument name="website" value="{{_defaultWebsite.name}}"/> @@ -50,7 +50,7 @@ stepKey="disableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml index 79c7571a08cfb..ac2790940b86f 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml @@ -23,14 +23,14 @@ </annotations> <before> - <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> - <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableFreeShippingMethodActionGroup" stepKey="enableFreeShipping"/> + <actionGroup ref="CliEnableFlatRateShippingMethodActionGroup" stepKey="enableFlatRateShipping"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="SimpleProduct2" stepKey="createProduct1"/> <createData entity="SimpleProduct2" stepKey="createProduct2"/> <createData entity="Simple_US_Customer_Assistance_Allowed_Two_Addresses" stepKey="createCustomer"/> @@ -43,8 +43,8 @@ <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> - <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml index 8169b9df4c43d..8afaaabbc92bf 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> @@ -59,7 +59,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml index 11d622319af33..8bef9fce9995b 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> @@ -59,7 +59,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login to storefront as Customer --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml index bc4c4adc3ac5a..1c3ae6e790089 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml @@ -23,7 +23,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> @@ -31,7 +31,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Admin Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml index e7b5de55a56cb..ae99a4dda5593 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml @@ -24,7 +24,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> </before> @@ -33,7 +33,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login into Customer account --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml index 5bbc218e0a948..51a4d1cf9fbed 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml @@ -22,7 +22,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createFirstCustomer"/> <createData entity="Simple_US_CA_Customer_Assistance_Allowed" stepKey="createSecondCustomer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> @@ -33,7 +33,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login into First Customer account --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml index 50513797d06e9..9c95ab3c3c5cc 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml @@ -24,7 +24,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> @@ -67,7 +67,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as new User --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml index d48f167656301..4032aff6d122f 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml @@ -26,7 +26,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> @@ -70,7 +70,7 @@ <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as new User --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml index e1ea363bdf6bc..10bff7c2ef68f 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml @@ -19,7 +19,7 @@ </annotations> <before> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createSimpleProduct"> <requiredEntity createDataKey="createCategory"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml index be2749e64f65c..38e89050c275e 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml @@ -33,7 +33,8 @@ <after> <closeTab stepKey="closeLoginAsCustomerTab"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml index 351a3c569ce24..6a83e820039d8 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml @@ -24,7 +24,7 @@ stepKey="enableLoginAsCustomer"/> <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" stepKey="enableLoginAsCustomerAutoDetection"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> <after> @@ -32,7 +32,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCacheAfterTestRun"/> </after> <!-- Login as Customer from Customer page --> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml index 7765975863485..5551ea1baba70 100644 --- a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml @@ -11,20 +11,18 @@ use Magento\LoginAsCustomerAssistance\ViewModel\ShoppingAssistanceViewModel; /** @var Escaper $escaper */ /** @var ShoppingAssistanceViewModel $viewModel */ $viewModel = $block->getViewModel(); -?> -<script type="text/x-magento-init"> -{ - ".form-create-account, .form-edit-account": { - "Magento_LoginAsCustomerAssistance/js/opt-in": { - "allowAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::ALLOWED ?>", - "denyAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::DENIED ?>" +if ($viewModel->isLoginAsCustomerEnabled()): ?> + <script type="text/x-magento-init"> + { + ".form-create-account, .form-edit-account": { + "Magento_LoginAsCustomerAssistance/js/opt-in": { + "allowAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::ALLOWED ?>", + "denyAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::DENIED ?>" + } } } -} -</script> - -<?php if ($viewModel->isLoginAsCustomerEnabled()): ?> + </script> <div class="field choice"> <input type="checkbox" name="assistance_allowed_checkbox" diff --git a/app/code/Magento/LoginAsCustomerGraphQl/Model/LoginAsCustomer/CreateCustomerToken.php b/app/code/Magento/LoginAsCustomerGraphQl/Model/LoginAsCustomer/CreateCustomerToken.php new file mode 100755 index 0000000000000..a10bc10ffb825 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/Model/LoginAsCustomer/CreateCustomerToken.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerGraphQl\Model\LoginAsCustomer; + +use Magento\Customer\Model\CustomerFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Integration\Model\Oauth\TokenFactory; +use Magento\Store\Api\Data\StoreInterface; +use Exception; + +/** + * Create customer token from customer email + */ +class CreateCustomerToken +{ + /** + * @var CustomerFactory + */ + private $customerFactory; + + /** + * @var TokenFactory + */ + private $tokenModelFactory; + + /** + * @param TokenFactory $tokenModelFactory + * @param CustomerFactory $customerFactory + */ + public function __construct( + TokenFactory $tokenModelFactory, + CustomerFactory $customerFactory + ) { + $this->tokenModelFactory = $tokenModelFactory; + $this->customerFactory= $customerFactory; + } + + /** + * Get admin user token + * + * @param string $email + * @param StoreInterface $store + * @return array + * @throws GraphQlInputException + * @throws LocalizedException + */ + public function execute(string $email, StoreInterface $store): array + { + $customer = $this->customerFactory->create()->setWebsiteId((int)$store->getId())->loadByEmail($email); + + /* Check if customer email exist */ + if (!$customer->getId()) { + throw new GraphQlInputException( + __('Customer email provided does not exist') + ); + } + + try { + return [ + "customer_token" => $this->tokenModelFactory->create() + ->createCustomerToken($customer->getId())->getToken() + ]; + } catch (Exception $e) { + throw new LocalizedException( + __( + 'Unable to generate tokens. ' + . 'Please wait and try again later.' + ) + ); + } + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/RequestCustomerToken.php b/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/RequestCustomerToken.php new file mode 100755 index 0000000000000..6889f79fd1270 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/RequestCustomerToken.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerGraphQl\Model\Resolver; + +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\LoginAsCustomerApi\Api\ConfigInterface as LoginAsCustomerConfig; +use Magento\LoginAsCustomerGraphQl\Model\LoginAsCustomer\CreateCustomerToken; + +/** + * Gets customer token + */ +class RequestCustomerToken implements ResolverInterface +{ + /** + * @var LoginAsCustomerConfig + */ + private $config; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var CreateCustomerToken + */ + private $createCustomerToken; + + /** + * @param AuthorizationInterface $authorization + * @param LoginAsCustomerConfig $config + * @param CreateCustomerToken $createCustomerToken + */ + public function __construct( + AuthorizationInterface $authorization, + LoginAsCustomerConfig $config, + CreateCustomerToken $createCustomerToken + ) { + $this->authorization = $authorization; + $this->config = $config; + $this->createCustomerToken = $createCustomerToken; + } + + /** + * Get Customer Token using email + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return Value|mixed|void + * @throws GraphQlAuthorizationException|GraphQlNoSuchEntityException|LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + $isAllowedLogin = $this->authorization->isAllowed('Magento_LoginAsCustomer::login'); + $isAlllowedShoppingAssistance = $this->authorization->isAllowed('Magento_LoginAsCustomer::allow_shopping_assistance'); + $isEnabled = $this->config->isEnabled(); + + /* Get input params */ + try { + $args = $args['input']; + } catch (NoSuchEntityException $e) { + throw new GraphQlInputException(__('Check input params.')); + } + + if (empty(trim($args['customer_email'], " "))) { + throw new GraphQlInputException(__('Specify the "customer email" value.')); + } + + $this->validateUser($context); + + if (!$isAllowedLogin || !$isEnabled) { + throw new GraphQlAuthorizationException( + __('Login as Customer is disabled.') + ); + } + + if (!$isAlllowedShoppingAssistance) { + throw new GraphQlAuthorizationException( + __('Allow remote shopping assistance is disabled.') + ); + } + + return $this->createCustomerToken->execute( + $args['customer_email'], + $context->getExtensionAttributes()->getStore() + ); + } + + /** + * Check if its an admin user + * + * @param ContextInterface $context + * @throws GraphQlAuthorizationException + */ + private function validateUser(ContextInterface $context): void + { + if ($context->getUserType() !== 2 || $context->getUserId() === 0) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/isRemoteShoppingAssistanceAllowed.php b/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/isRemoteShoppingAssistanceAllowed.php new file mode 100755 index 0000000000000..6ab2a7386986d --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/Model/Resolver/isRemoteShoppingAssistanceAllowed.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled; + +/** + * Determines if the customer allows remote shopping assistance + */ +class isRemoteShoppingAssistanceAllowed implements ResolverInterface +{ + /** + * @var IsAssistanceEnabled + */ + private $isAssistanceEnabled; + + /** + * @param IsAssistanceEnabled $isAssistanceEnabled + */ + public function __construct( + IsAssistanceEnabled $isAssistanceEnabled + ) { + $this->isAssistanceEnabled = $isAssistanceEnabled; + } + + /** + * Determines if remote shopping assistance is allowed for the specified customer + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return Value|mixed|void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + return $this->isAssistanceEnabled->execute((int)$value['model']->getId()); + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/Plugin/DataObjectHelperPlugin.php b/app/code/Magento/LoginAsCustomerGraphQl/Plugin/DataObjectHelperPlugin.php new file mode 100644 index 0000000000000..b1c54c6d00df7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/Plugin/DataObjectHelperPlugin.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + + +namespace Magento\LoginAsCustomerGraphQl\Plugin; + + +use Magento\Customer\Api\Data\CustomerExtensionFactory; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Api\DataObjectHelper; +use Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled; + +class DataObjectHelperPlugin +{ + /** + * @var CustomerExtensionFactory + */ + private $customerExtensionFactory; + + /** + * @param CustomerExtensionFactory $customerExtensionFactory + */ + public function __construct( + CustomerExtensionFactory $customerExtensionFactory + ) { + $this->customerExtensionFactory = $customerExtensionFactory; + } + + /** + * Add assistance_allowed extension attribute value to Customer instance. + * + * @param DataObjectHelper $subject + * @param DataObjectHelper $result + * @param mixed $dataObject + * @param array $data + * @param string $interfaceName + * @return DataObjectHelper + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterPopulateWithArray( + DataObjectHelper $subject, + DataObjectHelper $result, + Object $dataObject, + array $data, + string $interfaceName + ) { + if ($interfaceName === CustomerInterface::class + && array_key_exists('allow_remote_shopping_assistance', $data)) { + $isLoginAsCustomerEnabled = $data['allow_remote_shopping_assistance']; + $extensionAttributes = $dataObject->getExtensionAttributes(); + if (null === $extensionAttributes) { + $extensionAttributes = $this->customerExtensionFactory->create(); + } + $extensionAttributes->setAssistanceAllowed( + $isLoginAsCustomerEnabled ? IsAssistanceEnabled::ALLOWED : IsAssistanceEnabled::DENIED + ); + $dataObject->setExtensionAttributes($extensionAttributes); + } + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/README.md b/app/code/Magento/LoginAsCustomerGraphQl/README.md new file mode 100755 index 0000000000000..4bedf92dfc238 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/README.md @@ -0,0 +1,3 @@ +# LoginAsCustomerGraphQl + +**LoginAsCustomerGraphQl** provides flexible login as a customer so a merchant or merchant admin can log into an end customer's account to assist them with their account. diff --git a/app/code/Magento/LoginAsCustomerGraphQl/composer.json b/app/code/Magento/LoginAsCustomerGraphQl/composer.json new file mode 100755 index 0000000000000..9b3e7ca2efbb7 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/composer.json @@ -0,0 +1,29 @@ +{ + "name": "magento/module-login-as-customer-graph-ql", + "description": "Flexible login as a customer so a merchant or merchant admin can log into an end customer's account to assist them with their account.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-login-as-customer-api": "*", + "magento/module-login-as-customer-assistance": "*", + "magento/module-integration": "*", + "magento/module-store": "*", + "magento/module-customer": "*" + }, + "suggest": { + "magento/module-login-as-customer": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\LoginAsCustomerGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/etc/di.xml b/app/code/Magento/LoginAsCustomerGraphQl/etc/di.xml new file mode 100644 index 0000000000000..e98bc71d872ca --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/etc/di.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\Api\DataObjectHelper"> + <plugin name="add_allow_remote_shopping_assistance_to_customer" + type="Magento\LoginAsCustomerGraphQl\Plugin\DataObjectHelperPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerGraphQl/etc/module.xml b/app/code/Magento/LoginAsCustomerGraphQl/etc/module.xml new file mode 100755 index 0000000000000..1d0d92eb3cbbd --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/etc/module.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" ?> +<!-- +/** +* Copyright © Magento, Inc. All rights reserved. +* See COPYING.txt for license details. +*/ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerGraphQl"> + <sequence> + <module name="Magento_LoginAsCustomerApi"/> + <module name="Magento_Customer"/> + <module name="Magento_Store"/> + <module name="Magento_CatalogGraphQl"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/LoginAsCustomerGraphQl/etc/schema.graphqls b/app/code/Magento/LoginAsCustomerGraphQl/etc/schema.graphqls new file mode 100755 index 0000000000000..296f5b23a8b5f --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/etc/schema.graphqls @@ -0,0 +1,32 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +type Mutation { + generateCustomerTokenAsAdmin( + input: GenerateCustomerTokenAsAdminInput! + ): GenerateCustomerTokenAsAdminOutput + @resolver(class: "Magento\\LoginAsCustomerGraphQl\\Model\\Resolver\\RequestCustomerToken") + @doc(description: "Request a customer token so that an administrator can perform remote shopping assistance") +} + +input GenerateCustomerTokenAsAdminInput { + customer_email: String! @doc(description: "The email address of the customer requesting remote shopping assistance") +} + +type GenerateCustomerTokenAsAdminOutput { + customer_token: String! @doc(description: "The generated customer token") +} + +type Customer { + allow_remote_shopping_assistance: Boolean! + @resolver(class: "Magento\\LoginAsCustomerGraphQl\\Model\\Resolver\\isRemoteShoppingAssistanceAllowed") + @doc(description: "Indicates whether the customer has enabled remote shopping assistance") +} + +input CustomerCreateInput { + allow_remote_shopping_assistance: Boolean @doc(description: "Indicates whether the customer has enabled remote shopping assistance") +} + +input CustomerUpdateInput { + allow_remote_shopping_assistance: Boolean @doc(description: "Indicates whether the customer has enabled remote shopping assistance") +} diff --git a/app/code/Magento/LoginAsCustomerGraphQl/registration.php b/app/code/Magento/LoginAsCustomerGraphQl/registration.php new file mode 100755 index 0000000000000..0981811982e6b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerGraphQl/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerGraphQl', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php index f61e34512bfe3..57a59ad800469 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -33,8 +33,8 @@ class Asset extends Select * @param UiComponentFactory $uiComponentFactory * @param FilterBuilder $filterBuilder * @param FilterModifier $filterModifier - * @param OptionSourceInterface $optionsProvider * @param GetContentByAssetIdsInterface $getContentIdentities + * @param OptionSourceInterface $optionsProvider * @param array $components * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -44,8 +44,8 @@ public function __construct( UiComponentFactory $uiComponentFactory, FilterBuilder $filterBuilder, FilterModifier $filterModifier, - OptionSourceInterface $optionsProvider = null, GetContentByAssetIdsInterface $getContentIdentities, + OptionSourceInterface $optionsProvider = null, array $components = [], array $data = [] ) { diff --git a/app/code/Magento/MediaStorage/App/Media.php b/app/code/Magento/MediaStorage/App/Media.php index 34c20aab40bcb..fd73519ebd290 100644 --- a/app/code/Magento/MediaStorage/App/Media.php +++ b/app/code/Magento/MediaStorage/App/Media.php @@ -187,7 +187,9 @@ public function launch(): ResponseInterface $this->mediaDirectoryPath = $config->getMediaDirectory(); $allowedResources = $config->getAllowedResources(); $isAllowed = $this->isAllowed; - if (!$isAllowed($this->relativeFileName, $allowedResources)) { + $fileAbsolutePath = $this->directoryPub->getAbsolutePath($this->relativeFileName); + $fileRelativePath = str_replace(rtrim($this->mediaDirectoryPath, '/') . '/', '', $fileAbsolutePath); + if (!$isAllowed($fileRelativePath, $allowedResources)) { throw new LogicException('The path is not allowed: ' . $this->relativeFileName); } } diff --git a/app/code/Magento/MediaStorage/Service/ImageResize.php b/app/code/Magento/MediaStorage/Service/ImageResize.php index d5ce1a7e20f98..9b0adcd161339 100644 --- a/app/code/Magento/MediaStorage/Service/ImageResize.php +++ b/app/code/Magento/MediaStorage/Service/ImageResize.php @@ -195,14 +195,18 @@ public function resizeFromThemes(array $themes = null): Generator $this->fileStorageDatabase->saveFileToFilesystem($mediastoragefilename); } if ($this->mediaDirectory->isFile($originalImagePath)) { - foreach ($viewImages as $viewImage) { - $this->resize($viewImage, $originalImagePath, $originalImageName); + try { + foreach ($viewImages as $viewImage) { + $this->resize($viewImage, $originalImagePath, $originalImageName); + } + } catch (\Exception $e) { + $error = $e->getMessage(); } } else { $error = __('Cannot resize image "%1" - original image not found', $originalImagePath); } - yield ['filename' => $originalImageName, 'error' => $error] => $count; + yield ['filename' => $originalImageName, 'error' => (string) $error] => $count; } } @@ -276,6 +280,7 @@ private function getUniqueImageIndex(array $imageData): string * @param string $originalImagePath * @param array $imageParams * @return Image + * @throws \InvalidArgumentException */ private function makeImage(string $originalImagePath, array $imageParams): Image { diff --git a/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php b/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php index 068732a7225cd..e9877a6e1b612 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php @@ -118,7 +118,7 @@ public function testProcessRequestCreatesConfigFileMediaDirectoryIsNotProvided() ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects(self::once()) + $this->directoryPubMock->expects(self::exactly(2)) ->method('getAbsolutePath') ->with(self::RELATIVE_FILE_PATH) ->willReturn($filePath); @@ -154,7 +154,7 @@ public function testProcessRequestReturnsFileIfItsProperlySynchronized(): void ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(true); - $this->directoryPubMock->expects(self::once()) + $this->directoryPubMock->expects(self::exactly(2)) ->method('getAbsolutePath') ->with(self::RELATIVE_FILE_PATH) ->willReturn($filePath); @@ -214,10 +214,15 @@ public function testCatchException(bool $isDeveloper, int $setBodyCalls): void public function testExceptionWhenIsAllowedReturnsFalse(): void { + $filePath = '/absolute/path/to/test/file.png'; $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); + $this->directoryPubMock->expects(self::once()) + ->method('getAbsolutePath') + ->with(self::RELATIVE_FILE_PATH) + ->willReturn($filePath); $this->configMock->expects(self::once()) ->method('save'); diff --git a/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php b/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php index b8c4aded8a047..5df2269f8f80e 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/Service/ImageResizeTest.php @@ -60,11 +60,6 @@ class ImageResizeTest extends TestCase */ protected $imageFactoryMock; - /** - * @var Image|MockObject - */ - protected $imageMock; - /** * @var ParamsBuilder|MockObject */ @@ -141,7 +136,6 @@ protected function setUp(): void $this->appStateMock = $this->createMock(State::class); $this->imageConfigMock = $this->createMock(MediaConfig::class); $this->productImageMock = $this->createMock(ProductImage::class); - $this->imageMock = $this->createMock(Image::class); $this->imageFactoryMock = $this->createMock(ImageFactory::class); $this->paramsBuilderMock = $this->createMock(ParamsBuilder::class); $this->viewMock = $this->createMock(View::class); @@ -164,9 +158,6 @@ protected function setUp(): void ->with(DirectoryList::MEDIA) ->willReturn($this->mediaDirectoryMock); - $this->imageFactoryMock->expects($this->any()) - ->method('create') - ->willReturn($this->imageMock); $this->assetImageMock->expects($this->any()) ->method('getPath') ->willReturn($this->testfilepath); @@ -256,6 +247,11 @@ public function testResizeFromThemesMediaStorageDatabase() ->method('fileExists') ->willReturn(false); + $imageMock = $this->createMock(Image::class); + $this->imageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($imageMock); + $this->productImageMock->expects($this->any()) ->method('getCountUsedProductImages') ->willReturn(1); @@ -284,6 +280,49 @@ function () { $generator = $this->service->resizeFromThemes(['test-theme']); while ($generator->valid()) { + $resizeInfo = $generator->key(); + $this->assertEquals('image.jpg', $resizeInfo['filename']); + $this->assertEmpty($resizeInfo['error']); + $generator->next(); + } + } + + public function testResizeFromThemesUnsupportedImage() + { + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->willReturn(true); + $this->databaseMock->expects($this->any()) + ->method('fileExists') + ->willReturn(false); + + $this->imageFactoryMock->expects($this->once()) + ->method('create') + ->willThrowException(new \InvalidArgumentException('Unsupported image format.')); + + $this->productImageMock->expects($this->any()) + ->method('getCountUsedProductImages') + ->willReturn(1); + $this->productImageMock->expects($this->any()) + ->method('getUsedProductImages') + ->willReturnCallback( + function () { + $data = [[ 'filepath' => $this->testfilename ]]; + foreach ($data as $e) { + yield $e; + } + } + ); + + $this->mediaDirectoryMock->expects($this->any()) + ->method('isFile') + ->with($this->testfilepath) + ->willReturn(true); + + $generator = $this->service->resizeFromThemes(['test-theme']); + while ($generator->valid()) { + $resizeInfo = $generator->key(); + $this->assertEquals('Unsupported image format.', $resizeInfo['error']); $generator->next(); } } @@ -297,6 +336,11 @@ public function testResizeFromImageNameMediaStorageDatabase() ->method('fileExists') ->willReturn(false); + $imageMock = $this->createMock(Image::class); + $this->imageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($imageMock); + $this->mediaDirectoryMock->expects($this->any()) ->method('isFile') ->with($this->testfilepath) diff --git a/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php b/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php index ea3e765ebb5cc..03587f71036f2 100644 --- a/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php +++ b/app/code/Magento/Multishipping/Model/Cart/Controller/CartPlugin.php @@ -74,6 +74,7 @@ public function beforeDispatch(Cart $subject, RequestInterface $request) /** @var Quote $quote */ $quote = $this->checkoutSession->getQuote(); if ($quote->isMultipleShippingAddresses() && $this->isCheckoutComplete()) { + $this->disableMultishipping->execute($quote); foreach ($quote->getAllShippingAddresses() as $address) { $quote->removeAddress($address->getId()); } diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php index 8845395be406e..8bfff09aa73b2 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php @@ -1185,7 +1185,9 @@ private function validateMinimumAmountForAddressItems() $baseTotal = 0; foreach ($addresses as $address) { - $taxes = $taxInclude ? $address->getBaseTaxAmount() : 0; + $taxes = $taxInclude + ? $address->getBaseTaxAmount() + $address->getBaseDiscountTaxCompensationAmount() + : 0; $baseTotal += $address->getBaseSubtotalWithDiscount() + $taxes; } diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontNavigateToShippingInformationPageActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontNavigateToShippingInformationPageActionGroup.xml new file mode 100644 index 0000000000000..1ca3b20af6a84 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontNavigateToShippingInformationPageActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontNavigateToShippingInformationPageActionGroup"> + <annotations> + <description>Navigate to shipping information page. Starts on multishipping addressees page.</description> + </annotations> + + <waitForElementVisible selector="{{SingleShippingSection.goToShippingInfo}}" stepKey="waitForButton"/> + <click selector="{{SingleShippingSection.goToShippingInfo}}" stepKey="goToShippingInformation"/> + <waitForPageLoad stepKey="waitForShippingInfoPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontSelectMultipleAddressesOnCheckoutActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontSelectMultipleAddressesOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..f5e76a0d146fc --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontSelectMultipleAddressesOnCheckoutActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSelectMultipleAddressesOnCheckoutActionGroup"> + <annotations> + <description>Select addresses for multinshipping checkout. Start on multishipping addresses page.</description> + </annotations> + <arguments> + <argument name="addressOption1" type="string" defaultValue="1"/> + <argument name="addressOption2" type="string" defaultValue="2"/> + </arguments> + + <waitForElementVisible selector="{{MultishippingSection.shippingAddressOptions(addressOption1,addressOption1)}}" stepKey="waitForMultishippingPage"/> + <grabTextFrom selector="{{MultishippingSection.shippingAddressOptions(addressOption1,addressOption1)}}" stepKey="firstShippingAddressValue"/> + <selectOption selector="{{MultishippingSection.shippingAddressSelector(addressOption1)}}" userInput="{$firstShippingAddressValue}" stepKey="selectFirstShippingMethod"/> + <waitForPageLoad after="selectFirstShippingMethod" stepKey="waitForSecondShippingAddresses"/> + <grabTextFrom selector="{{MultishippingSection.shippingAddressOptions(addressOption2,addressOption2)}}" stepKey="secondShippingAddressValue"/> + <selectOption selector="{{MultishippingSection.shippingAddressSelector(addressOption2)}}" userInput="{$secondShippingAddressValue}" stepKey="selectSecondShippingMethod"/> + <click selector="{{SingleShippingSection.updateAddress}}" stepKey="clickOnUpdateAddress"/> + <waitForPageLoad stepKey="waitForShippingInformation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml index 7ae23e8f871eb..5e1c14c57f533 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml @@ -30,7 +30,7 @@ <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml index 27876df8caefe..dcdc9203a2075 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml @@ -30,7 +30,7 @@ <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml index f21a8d32d8841..043d0fc41abe7 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml @@ -30,7 +30,7 @@ <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml new file mode 100644 index 0000000000000..f0a97d240aa69 --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCreateOrderWithMultishippingAfterReturningToCartTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCreateOrderWithMultishippingAfterReturningToCartTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Checkout with multiple addresses."/> + <title value="Checkout with multiple addresses after returning on cart page during checkout."/> + <description value="Verify customer able to checkout with multiple addresses after returning to cart page and continue checkout with browser 'back' button."/> + <severity value="AVERAGE"/> + <testCaseId value="MC-39583"/> + <useCaseId value="MC-36425"/> + <group value="multishipping"/> + </annotations> + + <before> + <!--Create test data.--> + <createData entity="SimpleProduct2" stepKey="product"/> + <createData entity="Simple_US_Customer_Two_Addresses" stepKey="customer"/> + <!--Set up configuration.--> + <actionGroup ref="CliEnableFreeShippingMethodActionGroup" stepKey="enableFreeShipping"/> + <actionGroup ref="CliEnableFlatRateShippingMethodActionGroup" stepKey="enableFlatRateShipping"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> + </before> + + <after> + <!--Clean up test data, revert configuration.--> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> + </after> + + <!--Add product to cart and proceed to multishipping checkout. --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="navigateToProductPage"> + <argument name="productUrl" value="$product.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPageActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$product.name$"/> + <argument name="productQty" value="2"/> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <actionGroup ref="CheckingWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <actionGroup ref="SelectMultiShippingInfoActionGroup" stepKey="checkoutWithMultipleShipping"/> + <waitForPageLoad stepKey="waitForShippingInfoPage"/> + <!--Open cart page before place order.--> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="navigateToCartPage"/> + <waitForPageLoad stepKey="waitForCartPageLoad"/> + <!--Go back to continue checkout with multiple addresses again.--> + <moveBack stepKey="navigateBackToMultishippingCheckout"/> + <actionGroup ref="StorefrontSelectMultipleAddressesOnCheckoutActionGroup" stepKey="selectAddresses"/> + <actionGroup ref="StorefrontNavigateToShippingInformationPageActionGroup" stepKey="navigateToShippingInformationPage"/> + <actionGroup ref="SelectMultiShippingInfoActionGroup" stepKey="checkoutWithMultipleShippingAgain"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPaymentAgain"/> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="checkoutWithPaymentMethodAgain"/> + <actionGroup ref="ReviewOrderForMultiShipmentActionGroup" stepKey="reviewOrderForMultiShipment"> + <argument name="totalNameForFirstOrder" value="Shipping & Handling"/> + <argument name="totalPositionForFirstOrder" value="1"/> + <argument name="totalNameForSecondOrder" value="Shipping & Handling"/> + <argument name="totalPositionForSecondOrder" value="2"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPlaceOrderPageLoad"/> + <actionGroup ref="StorefrontPlaceOrderForMultipleAddressesActionGroup" stepKey="placeOrder"> + <argument name="firstOrderPosition" value="1"/> + <argument name="secondOrderPosition" value="2"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml index 2e5c0acc32053..cdf9c5683c57b 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontOrderWithMultishippingTest.xml @@ -27,9 +27,9 @@ <createData entity="SimpleProduct2" stepKey="createProduct2"/> <createData entity="Simple_US_Customer_Two_Addresses" stepKey="createCustomer"/> <!-- Set configurations --> - <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> - <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> - <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <actionGroup ref="CliEnableFreeShippingMethodActionGroup" stepKey="enableFreeShipping"/> + <actionGroup ref="CliEnableFlatRateShippingMethodActionGroup" stepKey="enableFlatRateShipping"/> + <actionGroup ref="CliEnableCheckMoneyOrderPaymentMethodActionGroup" stepKey="enableCheckMoneyOrderPaymentMethod"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$createCustomer$$"/> @@ -42,7 +42,7 @@ <!-- Need logout before customer delete. Fatal error appears otherwise --> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml index a696a693fa002..d5ef7e3d5c448 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/item/default.phtml @@ -6,18 +6,23 @@ // phpcs:disable Magento2.Files.LineLength ?> -<strong class="product name product-item-name"><a href="<?= $block->escapeUrl($block->getProductUrl()) ?>"><?= $block->escapeHtml($block->getProductName()) ?></a></strong> +<?php +/** @var \Magento\Checkout\Block\Cart\Item\Renderer\ $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<strong class="product name product-item-name"><a href="<?= $escaper->escapeUrl($block->getProductUrl()) ?>"><?= $escaper->escapeHtml($block->getProductName()) ?></a></strong> <?php if ($_options = $block->getOptionList()) : ?> <dl class="item-options"> <?php foreach ($_options as $_option) : ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> + <dt><?= $escaper->escapeHtml($_option['label']) ?></dt> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['span']) ?> + <?= $escaper->escapeHtml($_formatedOptionValue['value'], ['span', 'a']) ?> <?php if (isset($_formatedOptionValue['full_view'])) : ?> <dl class="item options tooltip content"> - <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <dd><?= $block->escapeHtml($_formatedOptionValue['full_view'], ['span']) ?></dd> + <dt><?= $escaper->escapeHtml($_option['label']) ?></dt> + <dd><?= $escaper->escapeHtml($_formatedOptionValue['full_view'], ['span']) ?></dd> </dl> <?php endif; ?> </dd> diff --git a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php index 6bdaa40019f8a..d3f8bcb8765c3 100644 --- a/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php +++ b/app/code/Magento/Newsletter/Model/Plugin/CustomerPlugin.php @@ -244,11 +244,21 @@ public function afterGetById(CustomerRepositoryInterface $subject, CustomerInter */ public function afterGetList(CustomerRepositoryInterface $subject, SearchResults $searchResults): SearchResults { + $customerEmails = []; + + foreach ($searchResults->getItems() as $customer) { + $customerEmails[] = $customer->getEmail(); + } + + $collection = $this->collectionFactory->create(); + $collection->addFieldToFilter('subscriber_email', ['in' => $customerEmails]); + foreach ($searchResults->getItems() as $customer) { /** @var CustomerExtensionInterface $extensionAttributes */ $extensionAttributes = $customer->getExtensionAttributes(); - - $isSubscribed = (int) $extensionAttributes->getIsSubscribed() === Subscriber::STATUS_SUBSCRIBED ?: false; + /** @var Subscriber $subscribe */ + $subscribe = $collection->getItemByColumnValue('subscriber_email', $customer->getEmail()); + $isSubscribed = $subscribe && (int) $subscribe->getStatus() === Subscriber::STATUS_SUBSCRIBED; $extensionAttributes->setIsSubscribed($isSubscribed); } diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml index e255f14a83661..a6cb2456c583f 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml @@ -20,7 +20,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Create a newsletter template that contains an image--> <amOnPage url="{{NewsletterTemplateForm.url}}" stepKey="amOnNewsletterTemplatePage"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddVariableToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddVariableToWYSIWYGNewsletterTest.xml index ff2e2a84a612e..d4bb8d358e21d 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddVariableToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddVariableToWYSIWYGNewsletterTest.xml @@ -20,7 +20,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Create Custom Variable--> <actionGroup ref="CreateCustomVariableActionGroup" stepKey="createCustomVariable" /> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml index 73880f283677d..0fa16d275d6f4 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml @@ -20,7 +20,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <amOnPage url="{{NewsletterTemplateForm.url}}" stepKey="amOnNewsletterTemplatePage"/> <waitForElementVisible selector="{{BasicFieldNewsletterSection.templateName}}" stepKey="waitForTemplateName"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateUpdateTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateUpdateTest.xml index 1a0c90ff13c36..1d8565d7b2b78 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateUpdateTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateUpdateTest.xml @@ -15,6 +15,9 @@ <title value="Newsletter Updating Test"/> <description value="Admin should be able update created Newsletter Template"/> <severity value="MAJOR"/> + <testCaseId value="MC-39506"/> + <useCaseId value="MAGETWO-69597"/> + <group value="newsletter"/> <group value="reports"/> <group value="mtf_migrated"/> <group value="WYSIWYGDisabled"/> @@ -40,6 +43,7 @@ </actionGroup> <actionGroup ref="AdminMarketingOpenNewsletterTemplateFromGridActionGroup" stepKey="openCreatedNewsletterTemplate"/> </before> + <after> <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterGridPage"> <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> @@ -74,8 +78,8 @@ <argument name="subject" value="{{updatedNewsletter.subject}}"/> </actionGroup> <actionGroup ref="AdminSearchNewsletterTemplateOnGridActionGroup" stepKey="findUpdatedNewsletterTemplate"> - <argument name="name" value="Updated Newsletter Template"/> - <argument name="subject" value="Updated Newsletter Subject"/> + <argument name="name" value="{{updatedNewsletter.name}}"/> + <argument name="subject" value="{{updatedNewsletter.subject}}"/> </actionGroup> <actionGroup ref="AdminMarketingOpenNewsletterTemplateFromGridActionGroup" stepKey="openTemplate"/> <actionGroup ref="AssertAdminNewsletterTemplateFormActionGroup" stepKey="assertNewsletterForm"> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnNewsletterTest.xml index 3a247402c111d..7cfd4c67369c8 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnNewsletterTest.xml @@ -20,7 +20,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <amOnPage url="{{NewsletterTemplateForm.url}}" stepKey="amOnNewsletterTemplatePage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> diff --git a/app/code/Magento/Newsletter/etc/extension_attributes.xml b/app/code/Magento/Newsletter/etc/extension_attributes.xml index 09925024e97d5..5c38c02c032b0 100644 --- a/app/code/Magento/Newsletter/etc/extension_attributes.xml +++ b/app/code/Magento/Newsletter/etc/extension_attributes.xml @@ -8,10 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> <extension_attributes for="Magento\Customer\Api\Data\CustomerInterface"> - <attribute code="is_subscribed" type="boolean" > - <join reference_table="newsletter_subscriber" reference_field="customer_id" join_on_field="entity_id"> - <field>subscriber_status</field> - </join> - </attribute> + <attribute code="is_subscribed" type="boolean"/> </extension_attributes> </config> diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliDisableCheckMoneyOrderPaymentMethodActionGroup.xml b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliDisableCheckMoneyOrderPaymentMethodActionGroup.xml new file mode 100644 index 0000000000000..4189a28e6746a --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliDisableCheckMoneyOrderPaymentMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliDisableCheckMoneyOrderPaymentMethodActionGroup"> + <annotations> + <description>Disable Check/Money order payment method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{DisableCheckMoneyOrderPaymentMethod.path}} {{DisableCheckMoneyOrderPaymentMethod.value}}" stepKey="disableCheckMoneyOrderPaymentMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliEnableCheckMoneyOrderPaymentMethodActionGroup.xml b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliEnableCheckMoneyOrderPaymentMethodActionGroup.xml new file mode 100644 index 0000000000000..155633b0772fd --- /dev/null +++ b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/CliEnableCheckMoneyOrderPaymentMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliEnableCheckMoneyOrderPaymentMethodActionGroup"> + <annotations> + <description>Enable Check/Money order payment method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFlatRateShippingMethodActionGroup.xml b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFlatRateShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..3b3e3d597e9f7 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFlatRateShippingMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliDisableFlatRateShippingMethodActionGroup"> + <annotations> + <description>Disable Flat Rate shipping method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{DisableFlatRateShippingMethod.path}} {{DisableFlatRateShippingMethod.value}}" stepKey="disableFlatRateShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFreeShippingMethodActionGroup.xml b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFreeShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..38f2176a95986 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliDisableFreeShippingMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliDisableFreeShippingMethodActionGroup"> + <annotations> + <description>Disable Free Shipping method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFlatRateShippingMethodActionGroup.xml b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFlatRateShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..f138a9b616289 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFlatRateShippingMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliEnableFlatRateShippingMethodActionGroup"> + <annotations> + <description>Enable Flat Rate shipping method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFreeShippingMethodActionGroup.xml b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFreeShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..05b7378466b6f --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/ActionGroup/CliEnableFreeShippingMethodActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CliEnableFreeShippingMethodActionGroup"> + <annotations> + <description>Enable Free Shipping method by CLI command config:set</description> + </annotations> + + <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShippingMethod"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/PageCache/Test/Mftf/ActionGroup/AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup.xml b/app/code/Magento/PageCache/Test/Mftf/ActionGroup/AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup.xml new file mode 100644 index 0000000000000..6ef5f878023a1 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/ActionGroup/AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup"> + <annotations> + <description>Assert that product page add to cart form key is different from cached value.</description> + </annotations> + <arguments> + <argument name="cachedValue" type="string"/> + </arguments> + + <grabValueFrom selector="{{StorefrontProductActionSection.inputFormKey}}" stepKey="grabUpdatedValue"/> + <assertRegExp stepKey="validateCachedFormKey"> + <expectedResult type="string">/\w{16}/</expectedResult> + <actualResult type="string">{{cachedValue}}</actualResult> + </assertRegExp> + <assertRegExp stepKey="validateUpdatedFormKey"> + <expectedResult type="string">/\w{16}/</expectedResult> + <actualResult type="variable">grabUpdatedValue</actualResult> + </assertRegExp> + <assertNotEquals stepKey="assertFormKeyUpdated"> + <expectedResult type="string">{{cachedValue}}</expectedResult> + <actualResult type="variable">grabUpdatedValue</actualResult> + </assertNotEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/StorefrontCachedInputFormKeyValueUpdatedTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/StorefrontCachedInputFormKeyValueUpdatedTest.xml new file mode 100644 index 0000000000000..a9d77429e3248 --- /dev/null +++ b/app/code/Magento/PageCache/Test/Mftf/Test/StorefrontCachedInputFormKeyValueUpdatedTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCachedInputFormKeyValueUpdatedTest"> + <annotations> + <features value="PageCache"/> + <stories value="FormKey"/> + <title value="Form Key value should be updated by js script"/> + <description value="Form Key value should be updated by js script"/> + <testCaseId value="MC-39300"/> + <useCaseId value="MC-30171"/> + <severity value="AVERAGE"/> + <group value="pageCache"/> + </annotations> + <before> + <!-- Create Data --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="full_page"/> + </actionGroup> + </before> + <after> + <!-- Delete data --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <grabValueFrom selector="{{StorefrontProductActionSection.inputFormKey}}" stepKey="grabCachedValue"/> + <resetCookie userInput="PHPSESSID" stepKey="resetSessionCookie"/> + <resetCookie userInput="form_key" stepKey="resetFormKeyCookie"/> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="reopenProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAddToCartFormKeyValueIsNotCachedActionGroup" stepKey="assertValueIsUpdatedByScript"> + <argument name="cachedValue" value="{$grabCachedValue}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/PageCache/ViewModel/FormKeyProvider.php b/app/code/Magento/PageCache/ViewModel/FormKeyProvider.php new file mode 100644 index 0000000000000..26f6be43c627a --- /dev/null +++ b/app/code/Magento/PageCache/ViewModel/FormKeyProvider.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\ViewModel; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\PageCache\Model\Config; + +/** + * Adds script to update form key from cookie after script rendering + */ +class FormKeyProvider implements ArgumentInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @param Config $config + */ + public function __construct( + Config $config + ) { + $this->config = $config; + } + + /** + * Is full page cache enabled + * + * @return bool + */ + public function isFullPageCacheEnabled(): bool + { + return $this->config->isEnabled(); + } +} diff --git a/app/code/Magento/PageCache/view/frontend/layout/default.xml b/app/code/Magento/PageCache/view/frontend/layout/default.xml index 7e1fc9d31b864..3db4b1c4ae52e 100644 --- a/app/code/Magento/PageCache/view/frontend/layout/default.xml +++ b/app/code/Magento/PageCache/view/frontend/layout/default.xml @@ -10,6 +10,13 @@ <referenceBlock name="head.components"> <block class="Magento\Framework\View\Element\Js\Components" name="pagecache_page_head_components" template="Magento_PageCache::js/components.phtml"/> </referenceBlock> + <referenceBlock name="head.additional"> + <block name="form_key_provider" template="Magento_PageCache::form_key_provider.phtml"> + <arguments> + <argument name="form_key_provider" xsi:type="object">Magento\PageCache\ViewModel\FormKeyProvider</argument> + </arguments> + </block> + </referenceBlock> <referenceContainer name="content"> <block class="Magento\PageCache\Block\Javascript" template="Magento_PageCache::javascript.phtml" name="pageCache" as="pageCache"/> </referenceContainer> diff --git a/app/code/Magento/PageCache/view/frontend/requirejs-config.js b/app/code/Magento/PageCache/view/frontend/requirejs-config.js index 7a33e2748b916..59d4499092965 100644 --- a/app/code/Magento/PageCache/view/frontend/requirejs-config.js +++ b/app/code/Magento/PageCache/view/frontend/requirejs-config.js @@ -8,5 +8,6 @@ var config = { '*': { pageCache: 'Magento_PageCache/js/page-cache' } - } + }, + deps: ['Magento_PageCache/js/form-key-provider'] }; diff --git a/app/code/Magento/PageCache/view/frontend/templates/form_key_provider.phtml b/app/code/Magento/PageCache/view/frontend/templates/form_key_provider.phtml new file mode 100644 index 0000000000000..4f952002e458f --- /dev/null +++ b/app/code/Magento/PageCache/view/frontend/templates/form_key_provider.phtml @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +if ($block->getFormKeyProvider()->isFullPageCacheEnabled()): ?> + <script type="text/x-magento-init"> + { + "*": { + "Magento_PageCache/js/form-key-provider": {} + } + } + </script> +<?php endif; ?> diff --git a/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js new file mode 100644 index 0000000000000..c63d97840e946 --- /dev/null +++ b/app/code/Magento/PageCache/view/frontend/web/js/form-key-provider.js @@ -0,0 +1,93 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define(function () { + 'use strict'; + + return function () { + var formKey, + inputElements, + inputSelector = 'input[name="form_key"]'; + + /** + * Set form_key cookie + * @private + */ + function setFormKeyCookie(value) { + var expires, + secure, + date = new Date(), + isSecure = !!window.cookiesConfig && window.cookiesConfig.secure; + + date.setTime(date.getTime() + 86400000); + expires = '; expires=' + date.toUTCString(); + secure = isSecure ? '; secure' : ''; + + document.cookie = 'form_key=' + (value || '') + expires + secure + '; path=/'; + } + + /** + * Retrieves form key from cookie + * @private + */ + function getFormKeyCookie() { + var cookie, + i, + nameEQ = 'form_key=', + cookieArr = document.cookie.split(';'); + + for (i = 0; i < cookieArr.length; i++) { + cookie = cookieArr[i]; + + while (cookie.charAt(0) === ' ') { + cookie = cookie.substring(1, cookie.length); + } + + if (cookie.indexOf(nameEQ) === 0) { + return cookie.substring(nameEQ.length, cookie.length); + } + } + + return null; + } + + /** + * Generate form key string + * @private + */ + function generateFormKeyString() { + var result = '', + length = 16, + chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + while (length--) { + result += chars[Math.round(Math.random() * (chars.length - 1))]; + } + + return result; + } + + /** + * Init form_key inputs with value + * @private + */ + function initFormKey() { + formKey = getFormKeyCookie(); + + if (!formKey) { + formKey = generateFormKeyString(); + setFormKeyCookie(formKey); + } + inputElements = document.querySelectorAll(inputSelector); + + if (inputElements.length) { + Array.prototype.forEach.call(inputElements, function (element) { + element.setAttribute('value', formKey); + }); + } + } + + initFormKey(); + }; +}); diff --git a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js index 41a32ab8a49c8..d7214918c530d 100644 --- a/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js +++ b/app/code/Magento/PageCache/view/frontend/web/js/page-cache.js @@ -7,9 +7,10 @@ define([ 'jquery', 'domReady', 'consoleLogger', + 'Magento_PageCache/js/form-key-provider', 'jquery-ui-modules/widget', 'mage/cookies' -], function ($, domReady, consoleLogger) { +], function ($, domReady, consoleLogger, formKeyInit) { 'use strict'; /** @@ -99,6 +100,7 @@ define([ /** * FormKey Widget - this widget is generating from key, saves it to cookie and + * @deprecated see Magento/PageCache/view/frontend/web/js/form-key-provider.js */ $.widget('mage.formKey', { options: { @@ -298,8 +300,7 @@ define([ }); domReady(function () { - $('body') - .formKey(); + formKeyInit(); }); return { diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml index cea228ac7a344..d43e894b014ff 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -41,7 +41,7 @@ <after> <magentoCLI command="config:set {{StorefrontPaypalEnableTransferCartLineConfigData.path}} {{StorefrontPaypalEnableTransferCartLineConfigData.value}}" stepKey="enableTransferCartLine"/> <magentoCLI command="config:set {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.path}} {{StorefrontPaypalExpressAuthorizationPaymentActionOptionConfigData.value}}" stepKey="setPaymentAction"/> - <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> + <actionGroup ref="CliDisableFreeShippingMethodActionGroup" stepKey="disableFreeShipping"/> <!-- Delete product --> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml index a4d99ecbf7e61..7e3c4dab4588e 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml @@ -29,7 +29,7 @@ <!--Enable Advanced Setting--> <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> diff --git a/app/code/Magento/Persistent/Observer/EmulateCustomerObserver.php b/app/code/Magento/Persistent/Observer/EmulateCustomerObserver.php index 8429eabd19e8a..1ff81137de57b 100644 --- a/app/code/Magento/Persistent/Observer/EmulateCustomerObserver.php +++ b/app/code/Magento/Persistent/Observer/EmulateCustomerObserver.php @@ -6,6 +6,7 @@ namespace Magento\Persistent\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\NoSuchEntityException; /** * Class EmulateCustomer @@ -86,9 +87,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ $customer = $this->customerRepository->getById($this->_persistentSession->getSession()->getCustomerId()); if ($defaultShipping = $customer->getDefaultShipping()) { - /** @var \Magento\Customer\Model\Data\Address $address */ - $address = $this->addressRepository->getById($defaultShipping); - if ($address) { + $address = $this->getCustomerAddressById((int) $defaultShipping); + + if ($address !== null) { $this->_customerSession->setDefaultTaxShippingAddress( [ 'country_id' => $address->getCountryId(), @@ -102,8 +103,9 @@ public function execute(\Magento\Framework\Event\Observer $observer) } if ($defaultBilling = $customer->getDefaultBilling()) { - $address = $this->addressRepository->getById($defaultBilling); - if ($address) { + $address = $this->getCustomerAddressById((int) $defaultBilling); + + if ($address !== null) { $this->_customerSession->setDefaultTaxBillingAddress([ 'country_id' => $address->getCountryId(), 'region_id' => $address->getRegion() ? $address->getRegionId() : null, @@ -118,4 +120,19 @@ public function execute(\Magento\Framework\Event\Observer $observer) } return $this; } + + /** + * Returns customer address by id + * + * @param int $addressId + * @return \Magento\Customer\Api\Data\AddressInterface|null + */ + private function getCustomerAddressById(int $addressId) + { + try { + return $this->addressRepository->getById($addressId); + } catch (NoSuchEntityException $exception) { + return null; + } + } } diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml index e6fae229d29b1..26db75d289a6e 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyThatInformationAboutViewingComparisonWishlistIsPersistedUnderLongTermCookieTest.xml @@ -45,6 +45,7 @@ </actionGroup> </before> <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> <createData entity="PersistentLogoutClearEnabled" stepKey="persistentLogoutClearEnabled"/> <createData entity="DisableSynchronizeWidgetProductsWithBackendStorage" stepKey="disableSynchronizeWidgetProductsWithBackendStorage"/> @@ -52,8 +53,7 @@ <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromCustomer"/> + <comment userInput="BIC workaround" stepKey="logoutFromCustomer"/> <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyComparedProductsWidget"> <argument name="widget" value="RecentlyComparedProductsWidget"/> </actionGroup> diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/EmulateCustomerObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/EmulateCustomerObserverTest.php index 6c35ade65451b..2df36577b2931 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/EmulateCustomerObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/EmulateCustomerObserverTest.php @@ -131,14 +131,14 @@ public function testExecuteWhenSessionPersistAndCustomerNotLoggedIn() $customerMock ->expects($this->once()) ->method('getDefaultShipping') - ->willReturn('shippingId'); + ->willReturn(12345); $customerMock ->expects($this->once()) ->method('getDefaultBilling') - ->willReturn('billingId'); + ->willReturn(12346); $valueMap = [ - ['shippingId', $defaultShippingAddressMock], - ['billingId', $defaultBillingAddressMock] + [12345, $defaultShippingAddressMock], + [12346, $defaultBillingAddressMock] ]; $this->addressRepositoryMock->expects($this->any())->method('getById')->willReturnMap($valueMap); $this->customerSessionMock diff --git a/app/code/Magento/ProductVideo/Model/ResourceModel/Video.php b/app/code/Magento/ProductVideo/Model/ResourceModel/Video.php index 68b593f335797..42fdf8265ee83 100644 --- a/app/code/Magento/ProductVideo/Model/ResourceModel/Video.php +++ b/app/code/Magento/ProductVideo/Model/ResourceModel/Video.php @@ -39,7 +39,8 @@ public function loadByIds(array $ids) $this->getMainTable() )->where( 'value_id IN(?)', - $ids + $ids, + \Zend_Db::INT_TYPE ); return $this->getConnection()->fetchAll($select); diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index d2e900138cd06..48cd4b2eb2fad 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -2312,7 +2312,9 @@ public function validateMinimumAmount($multishipping = false) if (!$minOrderMulti) { foreach ($addresses as $address) { - $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; + $taxes = $taxInclude + ? $address->getBaseTaxAmount() + $address->getBaseDiscountTaxCompensationAmount() + : 0; foreach ($address->getQuote()->getItemsCollection() as $item) { /** @var \Magento\Quote\Model\Quote\Item $item */ $amount = $includeDiscount ? @@ -2327,7 +2329,9 @@ public function validateMinimumAmount($multishipping = false) } else { $baseTotal = 0; foreach ($addresses as $address) { - $taxes = ($taxInclude) ? $address->getBaseTaxAmount() : 0; + $taxes = $taxInclude + ? $address->getBaseTaxAmount() + $address->getBaseDiscountTaxCompensationAmount() + : 0; $baseTotal += $includeDiscount ? $address->getBaseSubtotalWithDiscount() + $taxes : $address->getBaseSubtotal() + $taxes; diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index aee86eb1f8935..1ad98aa3e0263 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -139,7 +139,7 @@ class Address extends AbstractAddress implements const ADDRESS_TYPE_BILLING = 'billing'; const ADDRESS_TYPE_SHIPPING = 'shipping'; - + private const CACHED_ITEMS_ALL = 'cached_items_all'; /** @@ -1217,7 +1217,9 @@ public function validateMinimumAmount() $storeId ); - $taxes = $taxInclude ? $this->getBaseTaxAmount() : 0; + $taxes = $taxInclude + ? $this->getBaseTaxAmount() + $this->getBaseDiscountTaxCompensationAmount() + : 0; return $includeDiscount ? ($this->getBaseSubtotalWithDiscount() + $taxes >= $amount) : @@ -1653,7 +1655,7 @@ public function setCustomerId($customerId) public function getEmail() { $email = $this->getData(self::KEY_EMAIL); - if (!$email && $this->getQuote()) { + if ($this->getQuote() && (!$email || $this->getQuote()->dataHasChangedFor('customer_email'))) { $email = $this->getQuote()->getCustomerEmail(); $this->setEmail($email); } diff --git a/app/code/Magento/Quote/Model/Quote/Plugin/UpdateQuoteStoreId.php b/app/code/Magento/Quote/Model/Quote/Plugin/UpdateQuoteStoreId.php new file mode 100644 index 0000000000000..bffa0084e35bd --- /dev/null +++ b/app/code/Magento/Quote/Model/Quote/Plugin/UpdateQuoteStoreId.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Quote\Plugin; + +use Magento\Quote\Model\Quote; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Updates quote store id. + */ +class UpdateQuoteStoreId +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param StoreManagerInterface $storeManager + */ + public function __construct( + StoreManagerInterface $storeManager + ) { + $this->storeManager = $storeManager; + } + + /** + * Update store id in requested quote by store id from request. + * + * @param Quote $subject + * @param Quote $result + * @return Quote + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterLoadByIdWithoutStore(Quote $subject, Quote $result): Quote + { + $storeId = $this->storeManager->getStore() + ->getId() ?: $this->storeManager->getDefaultStoreView() + ->getId(); + $result->setStoreId($storeId); + + return $result; + } +} diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml index a513b6747f612..d24154bcf9da2 100644 --- a/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml +++ b/app/code/Magento/Quote/Test/Mftf/Data/CartItemData.xml @@ -23,4 +23,9 @@ <var key="quote_id" entityKey="return" entityType="GuestCart"/> <var key="sku" entityKey="sku" entityType="product"/> </entity> + <entity name="FourCartItems" type="CartItem"> + <data key="qty">4</data> + <var key="quote_id" entityKey="return" entityType="GuestCart"/> + <var key="sku" entityKey="sku" entityType="product"/> + </entity> </entities> diff --git a/app/code/Magento/Quote/etc/webapi_rest/di.xml b/app/code/Magento/Quote/etc/webapi_rest/di.xml index a55d2146be156..6ed9909f04eb9 100644 --- a/app/code/Magento/Quote/etc/webapi_rest/di.xml +++ b/app/code/Magento/Quote/etc/webapi_rest/di.xml @@ -16,4 +16,7 @@ <type name="Magento\Quote\Api\GuestCartItemRepositoryInterface"> <plugin name="updateCartIdFromRequest" type="Magento\Quote\Plugin\UpdateCartId" /> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="updateQuoteStoreId" type="Magento\Quote\Model\Quote\Plugin\UpdateQuoteStoreId" /> + </type> </config> diff --git a/app/code/Magento/Quote/etc/webapi_soap/di.xml b/app/code/Magento/Quote/etc/webapi_soap/di.xml index 27d5ff7753425..4b7646b6e1ef3 100644 --- a/app/code/Magento/Quote/etc/webapi_soap/di.xml +++ b/app/code/Magento/Quote/etc/webapi_soap/di.xml @@ -13,4 +13,7 @@ <plugin name="accessControl" type="Magento\Quote\Model\QuoteRepository\Plugin\AccessChangeQuoteControl" /> <plugin name="authorization" type="Magento\Quote\Model\QuoteRepository\Plugin\Authorization" /> </type> + <type name="Magento\Quote\Model\Quote"> + <plugin name="updateQuoteStoreId" type="Magento\Quote\Model\Quote\Plugin\UpdateQuoteStoreId" /> + </type> </config> diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php index 21243a4545fa3..36848fa9d7f99 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/GetCartForUser.php @@ -7,12 +7,15 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; use Magento\Quote\Model\Quote; +use Magento\Store\Api\StoreRepositoryInterface; /** * Get cart @@ -29,16 +32,24 @@ class GetCartForUser */ private $cartRepository; + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + /** * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId * @param CartRepositoryInterface $cartRepository + * @param StoreRepositoryInterface $storeRepository */ public function __construct( MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + StoreRepositoryInterface $storeRepository = null ) { $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; $this->cartRepository = $cartRepository; + $this->storeRepository = $storeRepository ?: ObjectManager::getInstance()->get(StoreRepositoryInterface::class); } /** @@ -49,6 +60,7 @@ public function __construct( * @param int $storeId * @return Quote * @throws GraphQlAuthorizationException + * @throws GraphQlInputException * @throws GraphQlNoSuchEntityException * @throws NoSuchEntityException */ @@ -75,14 +87,7 @@ public function execute(string $cartHash, ?int $customerId, int $storeId): Quote throw new GraphQlNoSuchEntityException(__('The cart isn\'t active.')); } - if ((int)$cart->getStoreId() !== $storeId) { - throw new GraphQlNoSuchEntityException( - __( - 'Wrong store code specified for cart "%masked_cart_id"', - ['masked_cart_id' => $cartHash] - ) - ); - } + $this->updateCartCurrency($cart, $storeId); $cartCustomerId = (int)$cart->getCustomerId(); @@ -101,4 +106,34 @@ public function execute(string $cartHash, ?int $customerId, int $storeId): Quote } return $cart; } + + /** + * Sets cart currency based on specified store. + * + * @param Quote $cart + * @param int $storeId + * @throws GraphQlInputException + * @throws NoSuchEntityException + */ + private function updateCartCurrency(Quote $cart, int $storeId) + { + $cartStore = $this->storeRepository->getById($cart->getStoreId()); + $currentCartCurrencyCode = $cartStore->getCurrentCurrency()->getCode(); + if ((int)$cart->getStoreId() !== $storeId) { + $newStore = $this->storeRepository->getById($storeId); + if ($cartStore->getWebsite() !== $newStore->getWebsite()) { + throw new GraphQlInputException( + __('Can\'t assign cart to store in different website.') + ); + } + $cart->setStoreId($storeId); + $cart->setStoreCurrencyCode($newStore->getCurrentCurrency()); + $cart->setQuoteCurrencyCode($newStore->getCurrentCurrency()); + } elseif ($cart->getQuoteCurrencyCode() !== $currentCartCurrencyCode) { + $cart->setQuoteCurrencyCode($cartStore->getCurrentCurrency()); + } else { + return; + } + $this->cartRepository->save($cart); + } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index 71740488c4cea..fa5be95d34822 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -11,6 +11,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\QuoteRepository; /** @@ -18,6 +19,16 @@ */ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface { + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedQuoteId; + + /** + * @var GetCartForUser + */ + private $getCartForUser; + /** * @var AssignShippingAddressToCart */ @@ -34,15 +45,21 @@ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface private $quoteRepository; /** + * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + * @param GetCartForUser $getCartForUser * @param AssignShippingAddressToCart $assignShippingAddressToCart * @param GetShippingAddress $getShippingAddress * @param QuoteRepository|null $quoteRepository */ public function __construct( + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId, + GetCartForUser $getCartForUser, AssignShippingAddressToCart $assignShippingAddressToCart, GetShippingAddress $getShippingAddress, QuoteRepository $quoteRepository = null ) { + $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + $this->getCartForUser = $getCartForUser; $this->assignShippingAddressToCart = $assignShippingAddressToCart; $this->getShippingAddress = $getShippingAddress; $this->quoteRepository = $quoteRepository @@ -81,7 +98,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s throw $e; } $this->assignShippingAddressToCart->execute($cart, $shippingAddress); - // trigger quote re-evaluation after address change + + // reload updated cart & trigger quote re-evaluation after address change + $maskedId = $this->quoteIdToMaskedQuoteId->execute((int)$cart->getId()); + $cart = $this->getCartForUser->execute($maskedId, $context->getUserId(), $cart->getStoreId()); $this->quoteRepository->save($cart); } } diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemUidArgsProcessor.php new file mode 100644 index 0000000000000..1a53ae6f38190 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemUidArgsProcessor.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; +use Magento\Framework\GraphQl\Query\Uid; + +/** + * Category UID processor class for category uid and category id arguments + */ +class CartItemUidArgsProcessor implements ArgumentsProcessorInterface +{ + private const ID = 'cart_item_id'; + + private const UID = 'cart_item_uid'; + + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + + /** + * Process the removeItemFromCart arguments for uids + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $filterKey = 'input'; + $idFilter = $args[$filterKey][self::ID] ?? []; + $uidFilter = $args[$filterKey][self::UID] ?? []; + if (!empty($idFilter) + && !empty($uidFilter) + && $fieldName === 'removeItemFromCart') { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID]) + ); + } elseif (!empty($uidFilter)) { + $args[$filterKey][self::ID] = $this->uidEncoder->decode((string)$uidFilter); + unset($args[$filterKey][self::UID]); + } + return $args; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php new file mode 100644 index 0000000000000..85e744c026c43 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; +use Magento\Framework\GraphQl\Query\Uid; + +/** + * Category UID processor class for category uid and category id arguments + */ +class CartItemsUidArgsProcessor implements ArgumentsProcessorInterface +{ + private const ID = 'cart_item_id'; + + private const UID = 'cart_item_uid'; + + /** @var Uid */ + private $uidEncoder; + + /** + * @param Uid $uidEncoder + */ + public function __construct(Uid $uidEncoder) + { + $this->uidEncoder = $uidEncoder; + } + + /** + * Process the updateCartItems arguments for cart uids + * + * @param string $fieldName, + * @param array $args + * @return array + * @throws GraphQlInputException + */ + public function process( + string $fieldName, + array $args + ): array { + $filterKey = 'input'; + if (!empty($args[$filterKey]['cart_items'])) { + foreach ($args[$filterKey]['cart_items'] as $key => $cartItem) { + $idFilter = $cartItem[self::ID] ?? []; + $uidFilter = $cartItem[self::UID] ?? []; + if (!empty($idFilter) + && !empty($uidFilter) + && $fieldName === 'updateCartItems') { + throw new GraphQlInputException( + __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::UID]) + ); + } elseif (!empty($uidFilter)) { + $args[$filterKey]['cart_items'][$key][self::ID] = $this->uidEncoder->decode((string)$uidFilter); + unset($args[$filterKey]['cart_items'][$key][self::UID]); + } + } + } + return $args; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php index 3199668060ea5..9d0c19cbc8f9c 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOption.php @@ -7,7 +7,9 @@ namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item as QuoteItem; /** @@ -15,18 +17,30 @@ */ class CustomizableOption { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + /** * @var CustomizableOptionValueInterface */ private $customizableOptionValue; + /** @var Uid */ + private $uidEncoder; + /** * @param CustomizableOptionValueInterface $customOptionValueDataProvider + * @param Uid|null $uidEncoder */ public function __construct( - CustomizableOptionValueInterface $customOptionValueDataProvider + CustomizableOptionValueInterface $customOptionValueDataProvider, + Uid $uidEncoder = null ) { $this->customizableOptionValue = $customOptionValueDataProvider; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -56,6 +70,7 @@ public function getData(QuoteItem $cartItem, int $optionId): array return [ 'id' => $option->getId(), + 'customizable_option_uid' => $this->uidEncoder->encode((string) self::OPTION_TYPE . '/' . $option->getId()), 'label' => $option->getTitle(), 'type' => $option->getType(), 'values' => $selectedOptionValueData, diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php index 74ed403465009..d62c1951eda68 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Dropdown.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product\Option\Type\Select as SelectOptionType; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Quote\Model\Quote\Item\Option as SelectedOption; use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; @@ -18,18 +20,30 @@ */ class Dropdown implements CustomizableOptionValueInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + /** * @var PriceUnitLabel */ private $priceUnitLabel; + /** @var Uid */ + private $uidEncoder; + /** * @param PriceUnitLabel $priceUnitLabel + * @param Uid|null $uidEncoder */ public function __construct( - PriceUnitLabel $priceUnitLabel + PriceUnitLabel $priceUnitLabel, + Uid $uidEncoder = null ) { $this->priceUnitLabel = $priceUnitLabel; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -50,8 +64,15 @@ public function getData( $optionPriceType = (string)$optionValue->getPriceType(); $priceValueUnits = $this->priceUnitLabel->getData($optionPriceType); + $optionDetails = [ + self::OPTION_TYPE, + $option->getOptionId(), + $optionValue->getOptionTypeId() + ]; + $selectedOptionValueData = [ 'id' => $selectedOption->getId(), + 'customizable_option_value_uid' => $this->uidEncoder->encode((string) implode('/', $optionDetails)), 'label' => $optionTypeRenderer->getFormattedOptionValue($selectedValue), 'value' => $selectedValue, 'price' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php index b3fa22c0cf61c..8831ee6304398 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Multiple.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product\Option\Type\DefaultType; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Quote\Model\Quote\Item\Option as SelectedOption; use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; @@ -18,18 +20,30 @@ */ class Multiple implements CustomizableOptionValueInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + /** * @var PriceUnitLabel */ private $priceUnitLabel; + /** @var Uid */ + private $uidEncoder; + /** * @param PriceUnitLabel $priceUnitLabel + * @param Uid|null $uidEncoder */ public function __construct( - PriceUnitLabel $priceUnitLabel + PriceUnitLabel $priceUnitLabel, + Uid $uidEncoder = null ) { $this->priceUnitLabel = $priceUnitLabel; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -51,8 +65,15 @@ public function getData( $optionValue = $option->getValueById($optionId); $priceValueUnits = $this->priceUnitLabel->getData($optionValue->getPriceType()); + $optionDetails = [ + self::OPTION_TYPE, + $option->getOptionId(), + $optionValue->getOptionTypeId() + ]; + $selectedOptionValueData[] = [ 'id' => $selectedOption->getId(), + 'customizable_option_value_uid' => $this->uidEncoder->encode((string)implode('/', $optionDetails)), 'label' => $optionValue->getTitle(), 'value' => $optionId, 'price' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php index 96f11badac82e..47e616d6094b9 100644 --- a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/CustomizableOptionValue/Text.php @@ -9,6 +9,8 @@ use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product\Option\Type\Text as TextOptionType; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Quote\Model\Quote\Item\Option as SelectedOption; use Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface; @@ -18,18 +20,30 @@ */ class Text implements CustomizableOptionValueInterface { + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + /** * @var PriceUnitLabel */ private $priceUnitLabel; + /** @var Uid */ + private $uidEncoder; + /** * @param PriceUnitLabel $priceUnitLabel + * @param Uid|null $uidEncoder */ public function __construct( - PriceUnitLabel $priceUnitLabel + PriceUnitLabel $priceUnitLabel, + Uid $uidEncoder = null ) { $this->priceUnitLabel = $priceUnitLabel; + $this->uidEncoder = $uidEncoder ?: ObjectManager::getInstance() + ->get(Uid::class); } /** @@ -47,6 +61,9 @@ public function getData( $selectedOptionValueData = [ 'id' => $selectedOption->getId(), + 'customizable_option_value_uid' => $this->uidEncoder->encode( + self::OPTION_TYPE . '/' . $option->getOptionId() + ), 'label' => '', 'value' => $optionTypeRenderer->getFormattedOptionValue($selectedOption->getValue()), 'price' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php index 2948994cf0ba3..2135f3798d190 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddSimpleProductsToCart.php @@ -63,6 +63,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->addProductsToCart->execute($cart, $cartItems); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php index ddd7d25943baa..6a53d976d59b3 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ApplyCouponToCart.php @@ -85,6 +85,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new LocalizedException(__($e->getMessage()), $e); } + $cart = $this->getCartForUser->execute($maskedCartId, $currentUserId, $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php index f0d97780845e8..d4ced5b8b97b0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php @@ -60,7 +60,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return [ 'price' => [ 'currency' => $currencyCode, - 'value' => $cartItem->getPrice(), + 'value' => $cartItem->getCalculationPrice(), ], 'row_total' => [ 'currency' => $currencyCode, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php index 39cf287a518b4..533e697c05123 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItems.php @@ -12,6 +12,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Item as QuoteItem; @@ -27,12 +28,19 @@ class CartItems implements ResolverInterface */ private $getCartProducts; + /** @var Uid */ + private $uidEncoder; + /** * @param GetCartProducts $getCartProducts + * @param Uid $uidEncoder */ - public function __construct(GetCartProducts $getCartProducts) - { + public function __construct( + GetCartProducts $getCartProducts, + Uid $uidEncoder + ) { $this->getCartProducts = $getCartProducts; + $this->uidEncoder = $uidEncoder; } /** @@ -68,6 +76,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $itemsData[] = [ 'id' => $cartItem->getItemId(), + 'uid' => $this->uidEncoder->encode((string) $cartItem->getItemId()), 'quantity' => $cartItem->getQty(), 'product' => $productData, 'model' => $cartItem, @@ -89,6 +98,7 @@ private function getCartProductsData(Quote $cart): array foreach ($products as $product) { $productsData[$product->getId()] = $product->getData(); $productsData[$product->getId()]['model'] = $product; + $productsData[$product->getId()]['uid'] = $this->uidEncoder->encode((string) $product->getId()); } return $productsData; diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php index c2045d4a0e8d5..09ef1ad581876 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php @@ -15,7 +15,9 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteId; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; /** * @inheritdoc @@ -32,16 +34,32 @@ class RemoveItemFromCart implements ResolverInterface */ private $cartItemRepository; + /** + * @var MaskedQuoteIdToQuoteId + */ + private $maskedQuoteIdToQuoteId; + + /** + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + /** * @param GetCartForUser $getCartForUser * @param CartItemRepositoryInterface $cartItemRepository + * @param MaskedQuoteIdToQuoteId $maskedQuoteIdToQuoteId + * @param ArgumentsProcessorInterface $argsSelection */ public function __construct( GetCartForUser $getCartForUser, - CartItemRepositoryInterface $cartItemRepository + CartItemRepositoryInterface $cartItemRepository, + MaskedQuoteIdToQuoteId $maskedQuoteIdToQuoteId, + ArgumentsProcessorInterface $argsSelection ) { $this->getCartForUser = $getCartForUser; $this->cartItemRepository = $cartItemRepository; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->argsSelection = $argsSelection; } /** @@ -49,27 +67,35 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (empty($args['input']['cart_id'])) { + $processedArgs = $this->argsSelection->process($info->fieldName, $args); + if (empty($processedArgs['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } - $maskedCartId = $args['input']['cart_id']; + $maskedCartId = $processedArgs['input']['cart_id']; + try { + $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId); + } catch (NoSuchEntityException $exception) { + throw new GraphQlNoSuchEntityException( + __('Could not find a cart with ID "%masked_cart_id"', ['masked_cart_id' => $maskedCartId]) + ); + } - if (empty($args['input']['cart_item_id'])) { + if (empty($processedArgs['input']['cart_item_id'])) { throw new GraphQlInputException(__('Required parameter "cart_item_id" is missing.')); } - $itemId = $args['input']['cart_item_id']; + $itemId = $processedArgs['input']['cart_item_id']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); - $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); try { - $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + $this->cartItemRepository->deleteById($cartId, $itemId); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__('The cart doesn\'t contain the item')); } catch (LocalizedException $e) { throw new GraphQlInputException(__($e->getMessage()), $e); } + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php index eb82510003fc7..55725e9fcce2b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetBillingAddressOnCart.php @@ -69,6 +69,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->checkCartCheckoutAllowance->execute($cart); $this->setBillingAddressOnCart->execute($context, $cart, $billingAddress); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php index fb6c1e678f1f0..bc753d50db68a 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentMethodOnCart.php @@ -69,6 +69,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->checkCartCheckoutAllowance->execute($cart); $this->setPaymentMethodOnCart->execute($cart, $paymentData); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php index d86244b2d8fc3..66bea8e886a57 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingAddressesOnCart.php @@ -69,6 +69,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->checkCartCheckoutAllowance->execute($cart); $this->setShippingAddressesOnCart->execute($context, $cart, $shippingAddresses); + // reload updated cart + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php index e1cd9c18d9873..911078fd029f1 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetShippingMethodsOnCart.php @@ -69,6 +69,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); $this->checkCartCheckoutAllowance->execute($cart); $this->setShippingMethodsOnCart->execute($context, $cart, $shippingMethods); + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index 005baaad0e1e5..981f5f3603516 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -17,6 +17,7 @@ use Magento\Quote\Api\CartRepositoryInterface; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider; +use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface; /** * @inheritdoc @@ -39,18 +40,26 @@ class UpdateCartItems implements ResolverInterface private $updateCartItems; /** - * @param GetCartForUser $getCartForUser + * @var ArgumentsProcessorInterface + */ + private $argsSelection; + + /** + * @param GetCartForUser $getCartForUser * @param CartRepositoryInterface $cartRepository * @param UpdateCartItemsProvider $updateCartItems + * @param ArgumentsProcessorInterface $argsSelection */ public function __construct( GetCartForUser $getCartForUser, CartRepositoryInterface $cartRepository, - UpdateCartItemsProvider $updateCartItems + UpdateCartItemsProvider $updateCartItems, + ArgumentsProcessorInterface $argsSelection ) { $this->getCartForUser = $getCartForUser; $this->cartRepository = $cartRepository; $this->updateCartItems = $updateCartItems; + $this->argsSelection = $argsSelection; } /** @@ -58,19 +67,21 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (empty($args['input']['cart_id'])) { + $processedArgs = $this->argsSelection->process($info->fieldName, $args); + + if (empty($processedArgs['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } - $maskedCartId = $args['input']['cart_id']; + $maskedCartId = $processedArgs['input']['cart_id']; - if (empty($args['input']['cart_items']) - || !is_array($args['input']['cart_items']) + if (empty($processedArgs['input']['cart_items']) + || !is_array($processedArgs['input']['cart_items']) ) { throw new GraphQlInputException(__('Required parameter "cart_items" is missing.')); } - $cartItems = $args['input']['cart_items']; + $cartItems = $processedArgs['input']['cart_items']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); @@ -83,6 +94,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value throw new GraphQlInputException(__($e->getMessage()), $e); } + $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); return [ 'cart' => [ 'model' => $cart, diff --git a/app/code/Magento/QuoteGraphQl/etc/di.xml b/app/code/Magento/QuoteGraphQl/etc/di.xml index 35b52dd495c5a..8dd35ab7f300b 100644 --- a/app/code/Magento/QuoteGraphQl/etc/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/di.xml @@ -33,4 +33,12 @@ </argument> </arguments> </type> + <type name="Magento\Framework\GraphQl\Query\Resolver\ArgumentsCompositeProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="cart_item_id" xsi:type="object">Magento\QuoteGraphQl\Model\CartItem\CartItemUidArgsProcessor</item> + <item name="cart_items_id" xsi:type="object">Magento\QuoteGraphQl\Model\CartItem\CartItemsUidArgsProcessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index cc9d1803b3e31..bcbbb3dc97de3 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -53,13 +53,13 @@ input CartItemInput { sku: String! quantity: Float! parent_sku: String @doc(description: "For child products, the SKU of its parent product") - selected_options: [ID!] @doc(description: "The selected options for the base product, such as color or size") + selected_options: [ID!] @doc(description: "The selected options for the base product, such as color or size with unique ID for a `CustomizableRadioOption`, `CustomizableDropDownOption`, `ConfigurableProductOptionsValues`, etc. objects") entered_options: [EnteredOptionInput!] @doc(description: "An array of entered options for the base product, such as personalization text") } input CustomizableOptionInput { - id: Int! - value_string: String! + id: Int @doc(description: "The customizable option id of the product") + value_string: String! @doc(description: "The string value of the option") } input ApplyCouponToCartInput { @@ -73,14 +73,16 @@ input UpdateCartItemsInput { } input CartItemUpdateInput { - cart_item_id: Int! + cart_item_id: Int @doc(description: "Deprecated. Use `cart_item_uid` instead.") + cart_item_uid: ID @doc(description: "The unique ID for a `CartItemInterface` object") quantity: Float customizable_options: [CustomizableOptionInput!] } input RemoveItemFromCartInput { cart_id: String! - cart_item_id: Int! + cart_item_id: Int @doc(description: "Deprecated. Use `cart_item_uid` instead.") + cart_item_uid: ID @doc(description: "Required field. The unique ID for a `CartItemInterface` object") } input SetShippingAddressesOnCartInput { @@ -199,9 +201,9 @@ type PlaceOrderOutput { } type Cart { - id: ID! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\MaskedCartId") @doc(description: "The ID of the cart.") + id: ID! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\MaskedCartId") @doc(description: "The unique ID for a `Cart` object") items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems") - applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead ") + applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead") applied_coupons: [AppliedCoupon] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupons") @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code") email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail") shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses") @@ -328,7 +330,8 @@ type VirtualCartItem implements CartItemInterface @doc(description: "Virtual Car } interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") { - id: String! + id: String! @deprecated(reason: "Use `uid` instead") + uid: ID! @doc(description: "The unique ID for a `CartItemInterface` object") quantity: Float! prices: CartItemPrices @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemPrices") product: ProductInterface! @@ -348,7 +351,8 @@ type CartItemPrices { } type SelectedCustomizableOption { - id: Int! + id: Int! @deprecated(reason: "Use SelectedCustomizableOption.customizable_option_uid instead") + customizable_option_uid: ID! @doc(description: "The unique ID for a `CustomizableRadioOption`, `CustomizableDropDownOption`, `CustomizableMultipleOption`, etc. of `CustomizableOptionInterface` objects") label: String! is_required: Boolean! values: [SelectedCustomizableOptionValue!]! @@ -356,7 +360,8 @@ type SelectedCustomizableOption { } type SelectedCustomizableOptionValue { - id: Int! + id: Int! @deprecated(reason: "Use SelectedCustomizableOptionValue.customizable_option_value_uid instead") + customizable_option_value_uid: ID! @doc(description: "The unique ID for a `CustomizableMultipleValue`, `CustomizableRadioValue`, `CustomizableCheckboxValue`, `CustomizableDropDownValue`, etc. objects") label: String! value: String! price: CartItemSelectedOptionValuePrice! @@ -369,7 +374,7 @@ type CartItemSelectedOptionValuePrice { } type Order { - order_number: String! + order_number: String! @doc(description: "The unique ID for a `Order` object.") order_id: String @deprecated(reason: "The order_id field is deprecated, use order_number instead.") } diff --git a/app/code/Magento/RemoteStorage/Driver/Cache/CacheFactory.php b/app/code/Magento/RemoteStorage/Driver/Cache/CacheFactory.php index 394b36f6871de..703393b69ec6a 100644 --- a/app/code/Magento/RemoteStorage/Driver/Cache/CacheFactory.php +++ b/app/code/Magento/RemoteStorage/Driver/Cache/CacheFactory.php @@ -7,10 +7,15 @@ namespace Magento\RemoteStorage\Driver\Cache; +use League\Flysystem\Adapter\Local; use League\Flysystem\Cached\CacheInterface; use League\Flysystem\Cached\Storage\Memory; use League\Flysystem\Cached\Storage\Predis; +use League\Flysystem\Cached\Storage\Adapter; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; use Magento\RemoteStorage\Driver\DriverException; +use Magento\RemoteStorage\Driver\DriverPool; use Predis\Client; /** @@ -20,14 +25,32 @@ class CacheFactory { public const ADAPTER_PREDIS = 'predis'; public const ADAPTER_MEMORY = 'memory'; + public const ADAPTER_LOCAL = 'local'; private const CACHE_KEY = 'storage'; + private const CACHE_FILE = 'storage_cache.json'; /** * Cache for 30 days. */ private const CACHE_EXPIRATION = 30 * 86400; + /** + * @var string + */ + private $localCacheRoot; + + /** + * @param Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem) + { + $this->localCacheRoot = $filesystem->getDirectoryRead( + DirectoryList::VAR_DIR, + DriverPool::FILE + )->getAbsolutePath(); + } + /** * Create cache adapter. * @@ -47,6 +70,8 @@ public function create(string $adapter, array $config = []): CacheInterface return new Predis(new Client($config), self::CACHE_KEY, self::CACHE_EXPIRATION); case self::ADAPTER_MEMORY: return new Memory(); + case self::ADAPTER_LOCAL: + return new Adapter(new Local($this->localCacheRoot), self::CACHE_FILE, self::CACHE_EXPIRATION); } throw new DriverException(__('Cache adapter %1 is not supported', $adapter)); diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index 6ba98cdd107e8..d8ee5d609cb57 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -31,7 +31,13 @@ </arguments> </virtualType> <virtualType name="fullRemoteFilesystem" type="Magento\RemoteStorage\Filesystem" /> + <virtualType name="stdFilesystem" type="Magento\Framework\Filesystem" /> <preference for="Magento\Framework\Filesystem" type="customRemoteFilesystem"/> + <type name="Magento\RemoteStorage\Driver\Cache\CacheFactory"> + <arguments> + <argument name="filesystem" xsi:type="object">stdFilesystem</argument> + </arguments> + </type> <type name="Magento\Framework\Filesystem\Directory\TargetDirectory"> <arguments> <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> diff --git a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php index b6e55af96f4c1..a346ad4ede29b 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Customer/Collection.php @@ -213,7 +213,8 @@ protected function _addOrdersStatistics() \Magento\Sales\Model\Order::STATE_CANCELED )->where( 'orders.customer_id IN(?)', - $customerIds + $customerIds, + \Zend_Db::INT_TYPE )->group( 'orders.customer_id' ); diff --git a/app/code/Magento/Review/Ui/DataProvider/Product/ReviewDataProvider.php b/app/code/Magento/Review/Ui/DataProvider/Product/ReviewDataProvider.php index a9c011d4a4865..315739e94fc0c 100644 --- a/app/code/Magento/Review/Ui/DataProvider/Product/ReviewDataProvider.php +++ b/app/code/Magento/Review/Ui/DataProvider/Product/ReviewDataProvider.php @@ -5,14 +5,14 @@ */ namespace Magento\Review\Ui\DataProvider\Product; +use Magento\Framework\Api\Filter; use Magento\Framework\App\RequestInterface; -use Magento\Ui\DataProvider\AbstractDataProvider; -use Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory; use Magento\Review\Model\ResourceModel\Review\Product\Collection; -use Magento\Review\Model\Review; +use Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory; +use Magento\Ui\DataProvider\AbstractDataProvider; /** - * Class ReviewDataProvider + * DataProvider for product reviews * * @api * @@ -58,7 +58,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @since 100.1.0 */ public function getData() @@ -79,24 +79,42 @@ public function getData() } /** - * {@inheritdoc} - * @since 100.1.0 + * Returns prepared field name + * + * @param string $name + * @return string */ - public function addFilter(\Magento\Framework\Api\Filter $filter) + private function getPreparedField(string $name): string { - $field = $filter->getField(); + $preparedName = ''; - if (in_array($field, ['review_id', 'created_at', 'status_id'])) { - $filter->setField('rt.' . $field); + if (in_array($name, ['review_id', 'created_at', 'status_id'])) { + $preparedName = 'rt.' . $name; + } elseif (in_array($name, ['title', 'nickname', 'detail'])) { + $preparedName = 'rdt.' . $name; + } elseif ($name === 'review_created_at') { + $preparedName = 'rt.created_at'; } - if (in_array($field, ['title', 'nickname', 'detail'])) { - $filter->setField('rdt.' . $field); - } + return $preparedName ?: $name; + } - if ($field === 'review_created_at') { - $filter->setField('rt.created_at'); - } + /** + * @inheritDoc + */ + public function addOrder($field, $direction) + { + $this->getCollection()->setOrder($this->getPreparedField($field), $direction); + } + + /** + * @inheritdoc + * @since 100.1.0 + */ + public function addFilter(Filter $filter) + { + $field = $filter->getField(); + $filter->setField($this->getPreparedField($field)); parent::addFilter($filter); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php index e6a209b541198..9bb71d837cade 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Form/Account.php @@ -6,11 +6,24 @@ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; +use Magento\Backend\Block\Template\Context; +use Magento\Backend\Model\Session\Quote; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\GroupManagementInterface; +use Magento\Customer\Model\Metadata\Form; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\Data\FormFactory; +use Magento\Customer\Model\Metadata\FormFactory as MetadataFormFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Phrase; use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Sales\Model\AdminOrder\Create; +use Magento\Store\Model\ScopeInterface; /** * Create order account form @@ -25,46 +38,48 @@ class Account extends AbstractForm /** * Metadata form factory * - * @var \Magento\Customer\Model\Metadata\FormFactory + * @var MetadataFormFactory */ protected $_metadataFormFactory; /** * Customer repository * - * @var \Magento\Customer\Api\CustomerRepositoryInterface + * @var CustomerRepositoryInterface */ protected $customerRepository; /** - * @var \Magento\Framework\Api\ExtensibleDataObjectConverter + * @var ExtensibleDataObjectConverter */ protected $_extensibleDataObjectConverter; + private const XML_PATH_EMAIL_REQUIRED_CREATE_ORDER = 'customer/create_account/email_required_create_order'; + /** - * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Backend\Model\Session\Quote $sessionQuote - * @param \Magento\Sales\Model\AdminOrder\Create $orderCreate + * @param Context $context + * @param Quote $sessionQuote + * @param Create $orderCreate * @param PriceCurrencyInterface $priceCurrency - * @param \Magento\Framework\Data\FormFactory $formFactory - * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor - * @param \Magento\Customer\Model\Metadata\FormFactory $metadataFormFactory - * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository + * @param FormFactory $formFactory + * @param DataObjectProcessor $dataObjectProcessor + * @param MetadataFormFactory $metadataFormFactory + * @param CustomerRepositoryInterface $customerRepository * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter * @param array $data * @param GroupManagementInterface|null $groupManagement * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Backend\Model\Session\Quote $sessionQuote, - \Magento\Sales\Model\AdminOrder\Create $orderCreate, + Context $context, + Quote $sessionQuote, + Create $orderCreate, PriceCurrencyInterface $priceCurrency, - \Magento\Framework\Data\FormFactory $formFactory, - \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor, - \Magento\Customer\Model\Metadata\FormFactory $metadataFormFactory, - \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, - \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, + FormFactory $formFactory, + DataObjectProcessor $dataObjectProcessor, + MetadataFormFactory $metadataFormFactory, + CustomerRepositoryInterface $customerRepository, + ExtensibleDataObjectConverter $extensibleDataObjectConverter, array $data = [], ?GroupManagementInterface $groupManagement = null ) { @@ -103,7 +118,7 @@ public function getHeaderCssClass() /** * Return header text * - * @return \Magento\Framework\Phrase + * @return Phrase */ public function getHeaderText() { @@ -114,10 +129,12 @@ public function getHeaderText() * Prepare Form and add elements to form * * @return $this + * @throws LocalizedException + * @throws NoSuchEntityException */ protected function _prepareForm() { - /** @var \Magento\Customer\Model\Metadata\Form $customerForm */ + /** @var Form $customerForm */ $customerForm = $this->_metadataFormFactory->create('customer', 'adminhtml_checkout'); // prepare customer attributes to show @@ -170,6 +187,8 @@ protected function _addAdditionalFormElementData(AbstractElement $element) * Return Form Elements values * * @return array + * @throws LocalizedException + * @throws NoSuchEntityException */ public function getFormValues() { @@ -183,7 +202,7 @@ public function getFormValues() ? $this->_extensibleDataObjectConverter->toFlatArray( $customer, [], - \Magento\Customer\Api\Data\CustomerInterface::class + CustomerInterface::class ) : []; foreach ($this->getQuote()->getData() as $key => $value) { @@ -193,7 +212,7 @@ public function getFormValues() } if (array_key_exists('group_id', $data) && empty($data['group_id'])) { - $data['group_id'] = $this->groupManagement->getDefaultGroup($this->getQuote()->getStoreId())->getId(); + $data['group_id'] = $this->getSelectedGroupId(); } if ($this->getQuote()->getCustomerEmail()) { @@ -208,6 +227,8 @@ public function getFormValues() * * @param array $attributes * @return array + * @throws LocalizedException + * @throws NoSuchEntityException */ private function extractValuesFromAttributes(array $attributes): array { @@ -231,7 +252,24 @@ private function isEmailRequiredToCreateOrder() { return $this->_scopeConfig->getValue( self::XML_PATH_EMAIL_REQUIRED_CREATE_ORDER, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ScopeInterface::SCOPE_STORE ); } + + /** + * Retrieve selected group id + * + * @return string + * @throws LocalizedException + * @throws NoSuchEntityException + */ + private function getSelectedGroupId(): string + { + $selectedGroupId = $this->groupManagement->getDefaultGroup($this->getQuote()->getStoreId())->getId(); + $orderDetails = $this->getRequest()->getParam('order'); + if (!empty($orderDetails) && !empty($orderDetails['account']['group_id'])) { + $selectedGroupId = $orderDetails['account']['group_id']; + } + return $selectedGroupId; + } } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php index 3d1160c0ca4f2..63558c0290e2c 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/Save.php @@ -7,6 +7,7 @@ use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Backend\App\Action; +use Magento\Sales\Helper\Data as SalesData; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; @@ -34,21 +35,30 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac */ protected $resultForwardFactory; + /** + * @var SalesData + */ + private $salesData; + /** * @param Action\Context $context * @param \Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader $creditmemoLoader * @param CreditmemoSender $creditmemoSender * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + * @param SalesData $salesData */ public function __construct( Action\Context $context, \Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader $creditmemoLoader, CreditmemoSender $creditmemoSender, - \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory + \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory, + SalesData $salesData = null ) { $this->creditmemoLoader = $creditmemoLoader; $this->creditmemoSender = $creditmemoSender; $this->resultForwardFactory = $resultForwardFactory; + $this->salesData = $salesData ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(SalesData::class); parent::__construct($context); } @@ -108,7 +118,7 @@ public function execute() $doOffline = isset($data['do_offline']) ? (bool)$data['do_offline'] : false; $creditmemoManagement->refund($creditmemo, $doOffline); - if (!empty($data['send_email'])) { + if (!empty($data['send_email']) && $this->salesData->canSendNewCreditMemoEmail()) { $this->creditmemoSender->send($creditmemo); } diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php index f66ca37a47655..ae3c1af1e3195 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/Invoice/Save.php @@ -86,7 +86,8 @@ public function __construct( $this->shipmentFactory = $shipmentFactory; $this->invoiceService = $invoiceService; parent::__construct($context); - $this->salesData = $salesData ?? $this->_objectManager->get(SalesData::class); + $this->salesData = $salesData ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(SalesData::class); } /** @@ -213,7 +214,7 @@ public function execute() // send invoice/shipment emails try { - if (!empty($data['send_email']) || $this->salesData->canSendNewInvoiceEmail()) { + if (!empty($data['send_email']) && $this->salesData->canSendNewInvoiceEmail()) { $this->invoiceSender->send($invoice); } } catch (\Exception $e) { @@ -222,7 +223,7 @@ public function execute() } if ($shipment) { try { - if (!empty($data['send_email']) || $this->salesData->canSendNewShipmentEmail()) { + if (!empty($data['send_email']) && $this->salesData->canSendNewShipmentEmail()) { $this->shipmentSender->send($shipment); } } catch (\Exception $e) { diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 1f23e4480ec1c..5d621f1632837 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -674,7 +674,7 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q if (in_array($option['option_type'], ['date', 'date_time', 'time', 'file'])) { $product->setSkipCheckRequiredOption(false); $formattedOptions[$option['option_id']] = - $buyRequest->getDataByKey('options')[$option['option_id']]; + $buyRequest->getDataByKey('options')[$option['option_id']]; continue; } @@ -984,6 +984,7 @@ public function applySidebarData($data) ); if ($item->getId()) { $this->addProduct($item->getProduct(), $item->getBuyRequest()->toArray()); + $this->removeItem($itemId, 'wishlist'); } } } @@ -1661,7 +1662,8 @@ public function setAccountData($accountData) // emulate request $request = $form->prepareRequest($accountData); - $data = $form->extractData($request); + $requestScope = $request->getPostValue() ? 'order/account' : null; + $data = $form->extractData($request, $requestScope); $data = $form->restoreData($data); $customer = $this->customerFactory->create(); $this->dataObjectHelper->populateWithArray( diff --git a/app/code/Magento/Sales/Model/EmailSenderHandler.php b/app/code/Magento/Sales/Model/EmailSenderHandler.php index a201c1285ae49..3a7a5727d8341 100644 --- a/app/code/Magento/Sales/Model/EmailSenderHandler.php +++ b/app/code/Magento/Sales/Model/EmailSenderHandler.php @@ -132,8 +132,9 @@ public function sendEmails() /** @var \Magento\Sales\Model\AbstractModel $item */ foreach ($entityCollection->getItems() as $item) { if ($this->emailSender->send($item, true)) { - $this->entityResource->save( - $item->setEmailSent(true) + $this->entityResource->saveAttribute( + $item->setEmailSent(true), + 'email_sent' ); } } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddSimpleProductToOrderAndCheckCheckboxActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddSimpleProductToOrderAndCheckCheckboxActionGroup.xml new file mode 100644 index 0000000000000..67b10d6955775 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddSimpleProductToOrderAndCheckCheckboxActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddSimpleProductToOrderAndCheckCheckboxActionGroup" extends="AddSimpleProductToOrderActionGroup"> + <annotations> + <description>Adds the provided Simple Product to an Order. Checks if checkbox is checked. Fills in the provided Product Qty. Clicks on 'Add Selected Product(s) to Order'.</description> + </annotations> + + <seeCheckboxIsChecked selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="verifyProductChecked" after="selectProduct"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminFillAccountInformationOnCreateOrderPageActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminFillAccountInformationOnCreateOrderPageActionGroup.xml new file mode 100644 index 0000000000000..acf4ff8b43eca --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminFillAccountInformationOnCreateOrderPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillAccountInformationOnCreateOrderPageActionGroup"> + <arguments> + <argument name="group" defaultValue="{{GeneralCustomerGroup.code}}" type="string"/> + <argument name="email" type="string"/> + </arguments> + <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{group}}" stepKey="selectCustomerGroup"/> + <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{email}}" stepKey="fillCustomerEmail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceWithUpdatedProductQtyActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceWithUpdatedProductQtyActionGroup.xml new file mode 100644 index 0000000000000..0f602c42ade25 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceWithUpdatedProductQtyActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminInvoiceWithUpdatedProductQtyActionGroup" extends="AdminCreateInvoiceActionGroup"> + <annotations> + <description>Start order Invoicing. + Update product qty to invoice (there is one product in the Order). + Submit the invoice. + </description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + + <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="{{qty}}" stepKey="fillQtyField" after="waitForInvoicePage"/> + <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="clickUpdateQuantityButton" after="fillQtyField"/> + <waitForPageLoad stepKey="waitForPageRefreshed" after="clickUpdateQuantityButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/CreditMemoData.xml b/app/code/Magento/Sales/Test/Mftf/Data/CreditMemoData.xml new file mode 100644 index 0000000000000..dc6280f7b3444 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/CreditMemoData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + + <entity name="CreditMemo" type="CreditMemo"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + </entity> + +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/InvoiceData.xml b/app/code/Magento/Sales/Test/Mftf/Data/InvoiceData.xml new file mode 100644 index 0000000000000..b55ef96932100 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/InvoiceData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + + <entity name="Invoice" type="Invoice"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + </entity> + +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/ShipmentData.xml b/app/code/Magento/Sales/Test/Mftf/Data/ShipmentData.xml new file mode 100644 index 0000000000000..96d371d3aaf3a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Data/ShipmentData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + + <entity name="Shipment" type="Shipment"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + </entity> + +</entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/CreditMemoMeta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/CreditMemoMeta.xml new file mode 100644 index 0000000000000..b051c91ba3b10 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Metadata/CreditMemoMeta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCreditMemo" dataType="CreditMemo" type="create" auth="adminOauth" url="V1/order/{return}/refund" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CartItem"> + <field key="quote_id">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/InvoiceMeta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/InvoiceMeta.xml new file mode 100644 index 0000000000000..e0bbfa1cd6e5f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Metadata/InvoiceMeta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateInvoice" dataType="Invoice" type="create" auth="adminOauth" url="V1/order/{return}/invoice" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CartItem"> + <field key="quote_id">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Metadata/ShipmentMeta.xml b/app/code/Magento/Sales/Test/Mftf/Metadata/ShipmentMeta.xml new file mode 100644 index 0000000000000..1c9de02c6a2e7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Metadata/ShipmentMeta.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateShipment" dataType="Shipment" type="create" auth="adminOauth" url="V1/order/{return}/ship" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CartItem"> + <field key="quote_id">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Sales/Test/Mftf/Page/AdminShipmentPage.xml b/app/code/Magento/Sales/Test/Mftf/Page/AdminShipmentPage.xml index d35a8ab5c4538..41d58bb61942a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Page/AdminShipmentPage.xml +++ b/app/code/Magento/Sales/Test/Mftf/Page/AdminShipmentPage.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminShipmentPage" url="sales/shipment/" area="admin" module="Magento_Sales"> <section name="AdminShipmentGridSection"/> + <section name="AdminShipmentsGridFiltersSection"/> </page> </pages> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormCustomOptionsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormCustomOptionsSection.xml index 066aa4181e7ef..49d26d4973874 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormCustomOptionsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormCustomOptionsSection.xml @@ -10,7 +10,7 @@ <section name="AdminOrderFormCustomOptionsSection"> <element name="quantity" type="input" selector="//input[@id='product_composite_configure_input_qty']"/> <element name="file" type="file" selector="//input[@type='file'][contains(@class, 'product-custom-option')]" /> - <element name="buttonOk" type="button" selector="//button[contains(@class, 'action-primary')][@data-role='action']"/> + <element name="buttonOk" type="button" selector="//button[contains(@class, 'action-primary')][@data-role='action']" timeout="30"/> <element name="linkChange" type="text" selector="//div[contains(@class, 'entry-edit')]//a[contains(text(),'Change')]"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml index 4437f6e6775f2..fae0bd4589580 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormItemsOrderedSection.xml @@ -13,7 +13,7 @@ <element name="configureButtonBySku" type="button" selector="//div[@class='sku-configure-button']//span[contains(text(),'Configure')]"/> <element name="configureProductOk" type="button" selector="//div[@class='page-main-actions']//span[contains(text(),'OK')]"/> <element name="configureProductQtyField" type="input" selector="//*[@id='super-product-table']/tbody/tr[{{arg}}]/td[5]/input[1]" parameterized="true"/> - <element name="addProductToOrder" type="input" selector="//*[@title='Add Products to Order']"/> + <element name="addProductToOrder" type="input" selector="//*[@title='Add Products to Order']" timeout="30"/> <element name="itemsOrderedSummaryText" type="textarea" selector="//table[@class='data-table admin__table-primary order-tables']/tfoot/tr"/> <element name="configureSelectAttribute" type="select" selector="select[id*=attribute]"/> <element name="itemsSKU" type="text" selector="(//div[contains(@class, 'product-sku-block')])[{{productNumber}}]" parameterized="true"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index 02878e79f3d70..55cd30f1caf2e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -41,5 +41,6 @@ <element name="viewLink" type="text" selector="//td/div[contains(.,'{{orderID}}')]/../..//a[@class='action-menu-item']" parameterized="true"/> <element name="selectOrderID" type="checkbox" selector="//td/div[text()='{{orderId}}']/../preceding-sibling::td//input" parameterized="true" timeout="60"/> <element name="orderId" type="text" selector="//table[contains(@class, 'data-grid')]//div[contains(text(), '{{orderId}}')]" parameterized="true"/> + <element name="orderIdByIncrementId" type="text" selector="//input[@class='admin__control-checkbox' and @value={{incrId}}]/parent::label/parent::td/following-sibling::td" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminShipmentsGridFiltersSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminShipmentsGridFiltersSection.xml new file mode 100644 index 0000000000000..ad1ec54e213ee --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminShipmentsGridFiltersSection.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminShipmentsGridFiltersSection"> + <element name="orderNumber" type="input" selector="input[name='order_increment_id']"/> + </section> +</sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml index d1c94965640c5..12beba19b4375 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontOrderDetailsSection.xml @@ -14,6 +14,8 @@ <element name="billingAddressBlock" type="block" selector=".box-order-billing-address > .box-content > address"/> <element name="discountSalesRule" type="text" selector="tr.discount span.price"/> <element name="shippingTotalDescription" type="text" selector="#my-orders-table tr.shipping th.mark"/> + <element name="tax" type="text" selector=".totals-tax .price"/> + <element name="grandTotalIncludingTax" type="text" selector=".grand_total_incl .amount"/> <element name="grandTotalPrice" type="text" selector="tr.grand_total span.price"/> <element name="paymentMethod" type="text" selector=".box-order-billing-method dt.title"/> <element name="shippingMethod" type="text" selector=".box-order-shipping-method div.box-content"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml index 182549a6fe301..afede97556513 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -61,8 +61,10 @@ <click selector="{{AdminOrderFormItemsSection.updateItemsAndQuantities}}" stepKey="clickUpdateItemsAndQuantitiesButton"/> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml index 6ce9909d06be5..21ced2e2df278 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithZeroSubtotalCheckoutTest.xml @@ -35,7 +35,7 @@ <field key="price">10.00</field> </createData> - <!-- Create a slaes rule with fixed discount --> + <!-- Create a sales rule with fixed discount --> <createData entity="SalesRuleNoCouponWithFixedDiscount" stepKey="createSalesRule"/> </before> <after> @@ -47,7 +47,6 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!--Create new customer order--> <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> <argument name="customer" value="$$simpleCustomer$$"/> @@ -62,10 +61,20 @@ <actionGroup ref="AdminSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitOrder" /> + <!-- Start the consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{SalesRuleConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{SalesRuleConsumerData.messageLimit}}"/> + </actionGroup> + <!--Verify order information--> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> + <reloadPage stepKey="refreshPage"/> <grabTextFrom selector="|Order # (\d+)|" stepKey="orderId"/> + <!-- Refresh the page --> + <reloadPage stepKey="refreshPageAgain"/> + <!-- Cancel the Order --> <actionGroup ref="CancelPendingOrderActionGroup" stepKey="cancelPendingOrder"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml index baef605ad52af..13b91fa605bca 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderAddProductCheckboxTest.xml @@ -34,17 +34,20 @@ <argument name="customer" value="$$createSimpleCustomer$$"/> </actionGroup> - <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> - <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSkuFilterBundle"/> - <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchBundle"/> - <scrollTo selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" x="0" y="-100" stepKey="scrollToCheckColumn"/> - <checkOption selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="selectProduct"/> - <seeCheckboxIsChecked selector="{{AdminOrderFormItemsSection.rowCheck('1')}}" stepKey="verifyProductChecked"/> - + <actionGroup ref="AdminAddSimpleProductToOrderAndCheckCheckboxActionGroup" stepKey="clickAddProducts"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="fillSkuFilterBundle"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="clickSearchBundle"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="scrollToCheckColumn"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectProduct"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="verifyProductChecked"/> + <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createSimpleCustomer" stepKey="deleteSimpleCustomer"/> </after> </test> -</tests> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml index b5c9e9443d1f9..6b714e0d18726 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -45,9 +45,11 @@ </actionGroup> <!--Fill customer group information--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectGroup"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillEmail"/> - + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectGroup"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillEmail"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress"> <argument name="customer" value="Simple_US_Customer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml new file mode 100644 index 0000000000000..794d09226d87f --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelClosedAndCompleteTest.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMassOrdersCancelClosedAndCompleteTest"> + <annotations> + <stories value="Mass Update Orders"/> + <title value="Mass cancel orders in status Complete, Closed"/> + <description value="Try to cancel orders in status Complete, Closed"/> + <severity value="MAJOR"/> + <testCaseId value="MC-39905"/> + <group value="sales"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + + <createData entity="ApiCategory" stepKey="createCategory"/> + + <createData entity="defaultSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="GuestCart" stepKey="createGuestCartOne"/> + <createData entity="SimpleCartItem" stepKey="addCartItemOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddressOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + <updateData createDataKey="createGuestCartOne" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformationOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </updateData> + + <createData entity="Invoice" stepKey="invoiceOrderOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + + <createData entity="Shipment" stepKey="shipOrderOne"> + <requiredEntity createDataKey="createGuestCartOne"/> + </createData> + + <createData entity="GuestCart" stepKey="createGuestCartTwo"/> + <createData entity="SimpleCartItem" stepKey="addCartItemTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="GuestAddressInformation" stepKey="addGuestOrderAddressTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + <updateData createDataKey="createGuestCartTwo" entity="GuestOrderPaymentMethod" stepKey="sendGuestPaymentInformationTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </updateData> + + <createData entity="Invoice" stepKey="invoiceOrderTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + + <createData entity="CreditMemo" stepKey="refundOrderTwo"> + <requiredEntity createDataKey="createGuestCartTwo"/> + </createData> + + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> + + <grabTextFrom selector="{{AdminOrdersGridSection.orderIdByIncrementId($createGuestCartOne.return$)}}" stepKey="getOrderOneId"/> + <grabTextFrom selector="{{AdminOrdersGridSection.orderIdByIncrementId($createGuestCartTwo.return$)}}" stepKey="getOrderTwoId"/> + + <actionGroup ref="AdminTwoOrderActionOnGridActionGroup" stepKey="massActionCancel"> + <argument name="action" value="Cancel"/> + <argument name="orderId" value="$getOrderOneId"/> + <argument name="secondOrderId" value="$getOrderTwoId"/> + </actionGroup> + <see userInput="You cannot cancel the order(s)." stepKey="assertOrderCancelMassActionFailMessage"/> + + <actionGroup ref="AdminOrderFilterByOrderIdAndStatusActionGroup" stepKey="seeFirstOrder"> + <argument name="orderId" value="$getOrderOneId"/> + <argument name="orderStatus" value="Complete"/> + </actionGroup> + <see userInput="$getOrderOneId" selector="{{AdminOrdersGridSection.gridCell('1','ID')}}" stepKey="assertFirstOrderID"/> + <see userInput="Complete" selector="{{AdminOrdersGridSection.gridCell('1','Status')}}" stepKey="assertFirstOrderStatus"/> + + <actionGroup ref="AdminOrderFilterByOrderIdAndStatusActionGroup" stepKey="seeSecondOrder"> + <argument name="orderId" value="$getOrderTwoId"/> + <argument name="orderStatus" value="Closed"/> + </actionGroup> + <see userInput="$getOrderTwoId" selector="{{AdminOrdersGridSection.gridCell('1','ID')}}" stepKey="assertSecondOrderID"/> + <see userInput="Closed" selector="{{AdminOrdersGridSection.gridCell('1','Status')}}" stepKey="assertSecondStatus"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml index b337af3753db3..10b3ba05aa2bb 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml @@ -8,15 +8,18 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMassOrdersCancelCompleteAndClosedTest"> + <test name="AdminMassOrdersCancelCompleteAndClosedTest" deprecated="Use AdminMassOrdersCancelClosedAndCompleteTest instead"> <annotations> <stories value="Mass Update Orders"/> - <title value="Mass cancel orders in status Complete, Closed"/> + <title value="DEPRECATED. Mass cancel orders in status Complete, Closed"/> <description value="Try to cancel orders in status Complete, Closed"/> <severity value="MAJOR"/> <testCaseId value="MC-16183"/> <group value="sales"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use AdminMassOrdersCancelClosedAndCompleteTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml index ac0af3f5b80db..f8136a9071a1a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSaveInAddressBookCheckboxStateTest.xml @@ -47,6 +47,8 @@ <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSimpleProductToOrder"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> + <!-- By default checkbox 'Add to address book' must be unchecked --> + <dontSeeCheckboxIsChecked selector="{{AdminOrderFormBillingAddressSection.SaveAddress}}" stepKey="checkBoxAddBillingAddressIsUnchecked"/> <!-- Just in case uncheck and check 'Same as Billing Address checkbox' --> <comment userInput="Just in case uncheck and check 'Same as Billing Address checkbox'" stepKey="uncheckAndCheckAgain"/> <uncheckOption selector="{{AdminOrderFormShippingAddressSection.SameAsBilling}}" stepKey="unCheckSameAsShippingAddressCheckbox"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml index bd6a21e3112ca..addf978235af4 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml @@ -48,9 +48,11 @@ <scrollToTopOfPage stepKey="scrollToTopOfOrderFormPage" after="seePaymentMethodRequired"/> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="scrollToTopOfOrderFormPage"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> - + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup" after="scrollToTopOfOrderFormPage"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail" after="selectCustomerGroup"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress" after="fillCustomerEmail"> <argument name="customer" value="Simple_US_Customer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index 727aef99352ec..7c2309eeb7be3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -46,8 +46,10 @@ </actionGroup> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail" after="selectCustomerGroup"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress" after="fillCustomerEmail"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml index 2bedb16f3d1dc..db779a7340b07 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -45,8 +45,10 @@ </actionGroup> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail" after="selectCustomerGroup"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup" after="addSimpleProductToOrder"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail" after="selectCustomerGroup"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> <!--Fill wrong customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillWrongCustomerAddress" after="fillCustomerEmail"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml index 367c50359701c..08d5776b79ea0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml @@ -136,14 +136,15 @@ <see selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$simpleProduct.name$$" stepKey="seeSimpleProductInWishList"/> <see selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProductInWishList"/> - <!-- Add products to order from Wish List --> + <!-- Move products to order from Wish List --> <waitForElementVisible selector="{{AdminCreateOrderWishListSection.addProductToOrderCheckBox($$simpleProduct.name$$)}}" stepKey="waitForCheckBoxToVisible"/> <click selector="{{AdminCreateOrderWishListSection.addProductToOrderCheckBox($$simpleProduct.name$$)}}" stepKey="selectProductToAddToOrder"/> + <click selector="{{AdminCustomerCreateNewOrderSection.updateChangesBtn}}" stepKey="clickUpdateChangesButton"/> <click selector="{{AdminCreateOrderWishListSection.addConfigProductToOrder($$createConfigProduct.name$$)}}" stepKey="AddConfigurableProductToOrder"/> <waitForElementVisible selector="{{AdminOrderFormConfigureProductSection.optionSelect($$createConfigProductAttribute.default_frontend_label$$)}}" stepKey="waitForConfigurablePopover"/> <selectOption selector="{{AdminOrderFormConfigureProductSection.optionSelect($$createConfigProductAttribute.default_frontend_label$$)}}" userInput="$$getConfigAttributeOption1.label$$" stepKey="selectConfigurableOption"/> <click selector="{{AdminOrderFormConfigureProductSection.ok}}" stepKey="clickOkButton"/> - <click selector="{{AdminCustomerCreateNewOrderSection.updateChangesBtn}}" stepKey="clickOnUpdateButton"/> + <comment userInput="Action should be removed but replaced with comment due to backward compatibility" stepKey="clickOnUpdateButton"/> <waitForPageLoad stepKey="waitForAdminOrderItemsOrderedSectionPageLoad1"/> <!-- Assert Products in Order item section --> @@ -171,9 +172,9 @@ <dontSee selector="{{AdminCreateOrderShoppingCartSection.shoppingCartBlock}}" userInput="$$simpleProduct.name$$" stepKey="donSeeProductInShoppingCart"/> <dontSee selector="{{AdminCreateOrderShoppingCartSection.shoppingCartBlock}}" userInput="$$simpleProduct1.name$$" stepKey="dontSeeSecondProductInShoppingCart"/> - <!-- After move, assert products are present in Wish List section --> - <see selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$simpleProduct.name$$" stepKey="seeSimpleProductInWishList1"/> - <see selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProductInWishList1"/> + <!-- After move, assert products are not present in Wish List section --> + <dontSee selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$simpleProduct.name$$" stepKey="seeSimpleProductInWishList1"/> + <dontSee selector="{{AdminCreateOrderWishListSection.wishListBlock}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProductInWishList1"/> <!-- After move, assert products are present in order items section --> <see selector="{{AdminOrderItemsOrderedSection.productName}}" userInput="$$simpleProduct.name$$" stepKey="seeSimpleProductInOrderItemGrid1"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml index 0718783534925..2b60c1d7ba550 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml @@ -24,9 +24,8 @@ </createData> <!-- Create Customer Account --> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <!-- Reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <comment userInput="Adding the comment to replace 'indexer:reindex' command for preserving Backward Compatibility" stepKey="reindex"/> + <comment userInput="Adding the comment to replace 'cache:flush' command for preserving Backward Compatibility" stepKey="flushCache"/> </before> <after> <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php index 8ccb821f6399f..fef38f981e12d 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Creditmemo/SaveTest.php @@ -21,9 +21,13 @@ use Magento\Framework\Registry; use Magento\Framework\Session\Storage; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Api\CreditmemoManagementInterface; use Magento\Sales\Controller\Adminhtml\Order\Creditmemo\Save; use Magento\Sales\Controller\Adminhtml\Order\CreditmemoLoader; +use Magento\Sales\Helper\Data as SalesData; +use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Creditmemo; +use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -87,6 +91,16 @@ class SaveTest extends TestCase */ protected $resultRedirectMock; + /** + * @var CreditmemoSender|MockObject + */ + private $creditmemoSender; + + /** + * @var SalesData|MockObject + */ + private $salesData; + /** * Init model for future tests */ @@ -147,12 +161,32 @@ protected function setUp(): void $context = $helper->getObject(Context::class, $arguments); + $creditmemoManagement = $this->getMockBuilder(CreditmemoManagementInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->_objectManager->expects($this->any()) + ->method('create') + ->with(CreditmemoManagementInterface::class) + ->willReturn($creditmemoManagement); + $this->creditmemoSender = $this->getMockBuilder(CreditMemoSender::class) + ->disableOriginalConstructor() + ->setMethods(['send']) + ->getMock(); + $this->creditmemoSender->expects($this->any()) + ->method('send') + ->willReturn(true); + $this->salesData = $this->getMockBuilder(SalesData::class) + ->disableOriginalConstructor() + ->setMethods(['canSendNewCreditmemoEmail']) + ->getMock(); $this->memoLoaderMock = $this->createMock(CreditmemoLoader::class); $this->_controller = $helper->getObject( Save::class, [ 'context' => $context, 'creditmemoLoader' => $this->memoLoaderMock, + 'creditmemoSender' => $this->creditmemoSender, + 'salesData' => $this->salesData ] ); } @@ -258,4 +292,94 @@ protected function _setSaveActionExpectationForMageCoreException($data, $errorMe $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($errorMessage); $this->_sessionMock->expects($this->once())->method('setFormData')->with($data); } + + /** + * @return array + */ + public function testExecuteEmailsDataProvider() + { + /** + * string $sendEmail + * bool $emailEnabled + * bool $shouldEmailBeSent + */ + return [ + ['', false, false], + ['', true, false], + ['on', false, false], + ['on', true, true] + ]; + } + + /** + * @param string $sendEmail + * @param bool $emailEnabled + * @param bool $shouldEmailBeSent + * @dataProvider testExecuteEmailsDataProvider + */ + public function testExecuteEmails( + $sendEmail, + $emailEnabled, + $shouldEmailBeSent + ) { + $orderId = 1; + $creditmemoId = 2; + $invoiceId = 3; + $creditmemoData = ['items' => [], 'send_email' => $sendEmail]; + + $this->resultRedirectFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultRedirectMock); + $this->resultRedirectMock->expects($this->once()) + ->method('setPath') + ->with('sales/order/view', ['order_id' => $orderId]) + ->willReturnSelf(); + + $order = $this->createPartialMock( + Order::class, + [] + ); + + $creditmemo = $this->createPartialMock( + Creditmemo::class, + ['isValidGrandTotal', 'getOrder', 'getOrderId'] + ); + $creditmemo->expects($this->once()) + ->method('isValidGrandTotal') + ->willReturn(true); + $creditmemo->expects($this->once()) + ->method('getOrder') + ->willReturn($order); + $creditmemo->expects($this->once()) + ->method('getOrderId') + ->willReturn($orderId); + + $this->_requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['order_id', null, $orderId], + ['creditmemo_id', null, $creditmemoId], + ['creditmemo', null, $creditmemoData], + ['invoice_id', null, $invoiceId] + ] + ); + + $this->_requestMock->expects($this->any()) + ->method('getPost') + ->willReturn($creditmemoData); + + $this->memoLoaderMock->expects($this->once()) + ->method('load') + ->willReturn($creditmemo); + + $this->salesData->expects($this->any()) + ->method('canSendNewCreditmemoEmail') + ->willReturn($emailEnabled); + if ($shouldEmailBeSent) { + $this->creditmemoSender->expects($this->once()) + ->method('send'); + } + $this->assertEquals($this->resultRedirectMock, $this->_controller->execute()); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/SaveTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/SaveTest.php index a84eee24e2e99..4029cd8368343 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/SaveTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/Invoice/SaveTest.php @@ -8,16 +8,27 @@ namespace Magento\Sales\Test\Unit\Controller\Adminhtml\Order\Invoice; use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\Session; use Magento\Backend\Model\View\Result\Redirect; use Magento\Framework\App\Request\Http; use Magento\Framework\Data\Form\FormKey\Validator; +use Magento\Framework\DB\Transaction; use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\ObjectManager\ObjectManager as FrameworkObjectManager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Result\PageFactory; use Magento\Sales\Controller\Adminhtml\Order\Invoice\Save; +use Magento\Sales\Helper\Data as SalesData; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Service\InvoiceService; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SaveTest extends TestCase { /** @@ -50,6 +61,26 @@ class SaveTest extends TestCase */ protected $controller; + /** + * @var SalesData|MockObject + */ + private $salesData; + + /** + * @var InvoiceSender|MockObject + */ + private $invoiceSender; + + /** + * @var FrameworkObjectManager|MockObject + */ + private $objectManager; + + /** + * @var InvoiceService|MockObject + */ + private $invoiceService; + /** * SetUp method * @@ -98,11 +129,36 @@ protected function setUp(): void $contextMock->expects($this->any()) ->method('getMessageManager') ->willReturn($this->messageManagerMock); + $this->objectManager = $this->createPartialMock( + FrameworkObjectManager::class, + ['create','get'] + ); + $contextMock->expects($this->any()) + ->method('getObjectManager') + ->willReturn($this->objectManager); + $this->invoiceSender = $this->getMockBuilder(InvoiceSender::class) + ->disableOriginalConstructor() + ->setMethods(['send']) + ->getMock(); + $this->invoiceSender->expects($this->any()) + ->method('send') + ->willReturn(true); + $this->salesData = $this->getMockBuilder(SalesData::class) + ->disableOriginalConstructor() + ->setMethods(['canSendNewInvoiceEmail']) + ->getMock(); + $this->invoiceService = $this->getMockBuilder(InvoiceService::class) + ->disableOriginalConstructor() + ->setMethods(['prepareInvoice']) + ->getMock(); $this->controller = $objectManager->getObject( Save::class, [ 'context' => $contextMock, + 'invoiceSender' => $this->invoiceSender, + 'invoiceService' => $this->invoiceService, + 'salesData' => $this->salesData ] ); } @@ -137,4 +193,148 @@ public function testExecuteNotValidPost() $this->assertEquals($redirectMock, $this->controller->execute()); } + + /** + * @return array + */ + public function testExecuteEmailsDataProvider() + { + /** + * string $sendEmail + * bool $emailEnabled + * bool $shouldEmailBeSent + */ + return [ + ['', false, false], + ['', true, false], + ['on', false, false], + ['on', true, true] + ]; + } + + /** + * @param string $sendEmail + * @param bool $emailEnabled + * @param bool $shouldEmailBeSent + * @dataProvider testExecuteEmailsDataProvider + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testExecuteEmails( + $sendEmail, + $emailEnabled, + $shouldEmailBeSent + ) { + $redirectMock = $this->getMockBuilder(Redirect::class) + ->disableOriginalConstructor() + ->getMock(); + $redirectMock->expects($this->once()) + ->method('setPath') + ->with('sales/order/view') + ->willReturnSelf(); + + $this->resultPageFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($redirectMock); + $this->formKeyValidatorMock->expects($this->once()) + ->method('validate') + ->with($this->requestMock) + ->willReturn(true); + $this->requestMock->expects($this->once()) + ->method('isPost') + ->willReturn(true); + + $invoiceData = ['items' => [], 'send_email' => $sendEmail]; + + $orderId = 2; + $order = $this->createPartialMock( + Order::class, + ['load','getId','canInvoice'] + ); + $order->expects($this->once()) + ->method('load') + ->willReturn($order); + $order->expects($this->once()) + ->method('getId') + ->willReturn($orderId); + $order->expects($this->once()) + ->method('canInvoice') + ->willReturn(true); + + $invoice = $this->getMockBuilder(Invoice::class) + ->disableOriginalConstructor() + ->setMethods(['getTotalQty','getOrder','register']) + ->getMock(); + $invoice->expects($this->any()) + ->method('getTotalQty') + ->willReturn(1); + $invoice->expects($this->any()) + ->method('getOrder') + ->willReturn($order); + $invoice->expects($this->once()) + ->method('register') + ->willReturn($order); + + $this->invoiceService->expects($this->any()) + ->method('prepareInvoice') + ->willReturn($invoice); + + $saveTransaction = $this->getMockBuilder(Transaction::class) + ->disableOriginalConstructor() + ->setMethods(['addObject','save']) + ->getMock(); + $saveTransaction->expects($this->at(0)) + ->method('addObject') + ->with($invoice)->willReturnSelf(); + $saveTransaction->expects($this->at(1)) + ->method('addObject') + ->with($order)->willReturnSelf(); + $saveTransaction->expects($this->at(2)) + ->method('save'); + + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getCommentText']) + ->getMock(); + $session->expects($this->once()) + ->method('getCommentText') + ->with(true); + + $this->objectManager->expects($this->any()) + ->method('create') + ->will( + $this->returnValueMap( + [ + [Transaction::class, [], $saveTransaction], + [Order::class, [], $order], + [Session::class, [], $session] + ] + ) + ); + $this->objectManager->expects($this->any()) + ->method('get') + ->with(Session::class) + ->willReturn($session); + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['order_id', null, $orderId], + ['invoice', null, $invoiceData] + ] + ); + $this->requestMock->expects($this->any()) + ->method('getPost') + ->willReturn($invoiceData); + + $this->salesData->expects($this->any()) + ->method('canSendNewInvoiceEmail') + ->willReturn($emailEnabled); + if ($shouldEmailBeSent) { + $this->invoiceSender->expects($this->once()) + ->method('send'); + } + + $this->assertEquals($redirectMock, $this->controller->execute()); + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php index c587d2322c298..36f5b7c9f4cdd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/AdminOrder/CreateTest.php @@ -212,10 +212,15 @@ public function testSetAccountData() ->method('restoreData') ->willReturn(['group_id' => 1]); + $requestMock = $this->getMockBuilder(RequestInterface::class) + ->setMethods(['getPostValue']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $requestMock->expects($this->atLeastOnce())->method('getPostValue')->willReturn(null); $customerForm->method('prepareRequest') - ->willReturn($this->getMockForAbstractClass(RequestInterface::class)); + ->willReturn($requestMock); - $customer = $this->getMockForAbstractClass(CustomerInterface::class); + $customer = $this->createMock(CustomerInterface::class); $this->customerMapper->expects(self::atLeastOnce()) ->method('toFlatArray') ->willReturn(['group_id' => 1]); diff --git a/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php b/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php index 757a026aa5d68..2a7b44efa5261 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php @@ -100,7 +100,7 @@ protected function setUp(): void false, false, true, - ['save'] + ['saveAttribute'] ); $this->entityCollection = $this->getMockForAbstractClass( @@ -252,7 +252,7 @@ public function testExecute($configValue, $collectionItems, $emailSendingResult) $this->entityResource ->expects($this->once()) - ->method('save') + ->method('saveAttribute') ->with($collectionItem); } } diff --git a/app/code/Magento/Sales/etc/adminhtml/di.xml b/app/code/Magento/Sales/etc/adminhtml/di.xml index 35ef510d277bf..e221467dbcf90 100644 --- a/app/code/Magento/Sales/etc/adminhtml/di.xml +++ b/app/code/Magento/Sales/etc/adminhtml/di.xml @@ -48,4 +48,9 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\DataProvider\ProductCollection"> + <arguments> + <argument name="collectionFactory" xsi:type="object">\Magento\Catalog\Ui\DataProvider\Product\ProductCollectionFactory</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index 0e65131b7c4b0..13afa0832086e 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -804,3 +804,4 @@ If set YES Email field will be required during Admin order creation for new Cust "Please enter a coupon code!","Please enter a coupon code!" "Reorder is not available.","Reorder is not available." "The coupon code has been removed.","The coupon code has been removed." +"Add to address book","Add to address book" diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 80083569df889..638ac7e66f769 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -125,7 +125,7 @@ endif; ?> <?php endif; ?> class="admin__control-checkbox"/> <label for="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" - class="admin__field-label"><?= $block->escapeHtml(__('Save in address book')) ?></label> + class="admin__field-label"><?= $block->escapeHtml(__('Add to address book')) ?></label> </div> </div> <?php $hideElement = 'address-' . ($block->getIsShipping() ? 'shipping' : 'billing') . '-overlay'; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index ba93f5f88c387..9454fe17fe2d2 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -942,7 +942,8 @@ define([ */ sidebarConfigureProduct: function (listType, productId, itemId) { // create additional fields - var params = {}; + var params = {}, + isWishlist = !!itemId; params.reset_shipping = true; params.add_product = productId; this.prepareParams(params); @@ -963,10 +964,18 @@ define([ }.bind(this)); // response handler productConfigure.setOnLoadIFrameCallback(listType, function (response) { + var areas = ['items', 'shipping_method', 'billing_method', 'totals', 'giftmessage']; + if (!response.ok) { return; } - this.loadArea(['items', 'shipping_method', 'billing_method', 'totals', 'giftmessage'], true); + if (isWishlist) { + this.removeSidebarItem(itemId, 'wishlist').done(function () { + this.loadArea(areas, true); + }.bind(this)); + } else { + this.loadArea(areas, true); + } }.bind(this)); // show item configuration itemId = itemId ? itemId : productId; @@ -975,7 +984,10 @@ define([ }, removeSidebarItem: function (id, from) { - this.loadArea(['sidebar_' + from], 'sidebar_data_' + from, {remove_item: id, from: from}); + return this.loadArea(['sidebar_' + from], 'sidebar_data_' + from, { + remove_item: id, + from: from + }); }, itemsUpdate: function () { diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index 3544acd1564d0..8a76f51d78e79 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -39,7 +39,7 @@ type CustomerOrders @doc(description: "The collection of orders that match the c } type CustomerOrder @doc(description: "Contains details about each of the customer's orders") { - id: ID! @doc(description: "Unique identifier for the order") + id: ID! @doc(description: "The unique ID for a `CustomerOrder` object") order_date: String! @doc(description: "The date the order was placed") status: String! @doc(description: "The current status of the order") number: String! @doc(description: "The order number") @@ -65,7 +65,7 @@ type OrderAddress @doc(description: "OrderAddress contains detailed information lastname: String! @doc(description: "The family name of the person associated with the shipping/billing address") middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") region: String @doc(description: "The state or province name") - region_id: ID @doc(description: "The unique ID for a pre-defined region") + region_id: ID @doc(description: "The unique ID for a `Region` object of a pre-defined region") country_code: CountryCodeEnum @doc(description: "The customer's country") street: [String!]! @doc(description: "An array of strings that define the street number and name") company: String @doc(description: "The customer's company") @@ -79,7 +79,7 @@ type OrderAddress @doc(description: "OrderAddress contains detailed information } interface OrderItemInterface @doc(description: "Order item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\OrderItem") { - id: ID! @doc(description: "The unique identifier of the order item") + id: ID! @doc(description: "The unique ID for a `OrderItemInterface` object") product_name: String @doc(description: "The name of the base product") product_sku: String! @doc(description: "The SKU of the base product") product_url_key: String @doc(description: "URL key of the base product") @@ -123,7 +123,7 @@ type OrderTotal @doc(description: "Contains details about the sales total amount } type Invoice @doc(description: "Invoice details") { - id: ID! @doc(description: "The ID of the invoice, used for API purposes") + id: ID! @doc(description: "The unique ID for a `Invoice` object") number: String! @doc(description: "Sequential invoice number") total: InvoiceTotal @doc(description: "Invoice total amount details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoice\\InvoiceTotal") items: [InvoiceItemInterface] @doc(description: "Invoiced product details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoice\\InvoiceItems") @@ -131,7 +131,7 @@ type Invoice @doc(description: "Invoice details") { } interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\InvoiceItem") { - id: ID! @doc(description: "The unique ID of the invoice item") + id: ID! @doc(description: "The unique ID for a `InvoiceItemInterface` object") order_item: OrderItemInterface @doc(description: "Contains details about an individual order item") product_name: String @doc(description: "The name of the base product") product_sku: String! @doc(description: "The SKU of the base product") @@ -167,7 +167,7 @@ type ShippingDiscount @doc(description:"Defines an individual shipping discount. } type OrderShipment @doc(description: "Order shipment details") { - id: ID! @doc(description: "The unique ID of the shipment") + id: ID! @doc(description: "The unique ID for a `OrderShipment` object") number: String! @doc(description: "The sequential credit shipment number") tracking: [ShipmentTracking] @doc(description: "Contains shipment tracking details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipment\\ShipmentTracking") items: [ShipmentItemInterface] @doc(description: "Contains items included in the shipment") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipment\\ShipmentItems") @@ -180,7 +180,7 @@ type SalesCommentItem @doc(description: "Comment item details") { } interface ShipmentItemInterface @doc(description: "Order shipment item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\ShipmentItem"){ - id: ID! @doc(description: "Shipment item unique identifier") + id: ID! @doc(description: "The unique ID for a `ShipmentItemInterface` object") order_item: OrderItemInterface @doc(description: "Associated order item") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItem") product_name: String @doc(description: "Name of the base product") product_sku: String! @doc(description: "SKU of the base product") @@ -204,7 +204,7 @@ type OrderPaymentMethod @doc(description: "Contains details about the payment me } type CreditMemo @doc(description: "Credit memo details") { - id: ID! @doc(description: "The unique ID of the credit memo, used for API purposes") + id: ID! @doc(description: "The unique ID for a `CreditMemo` object") number: String! @doc(description: "The sequential credit memo number") items: [CreditMemoItemInterface] @doc(description: "An array containing details about refunded items") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoItems") total: CreditMemoTotal @doc(description: "Contains details about the total refunded amount") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoTotal") @@ -212,7 +212,7 @@ type CreditMemo @doc(description: "Credit memo details") { } interface CreditMemoItemInterface @doc(description: "Credit memo item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\CreditMemoItem") { - id: ID! @doc(description: "The unique ID of the credit memo item, used for API purposes") + id: ID! @doc(description: "The unique ID for a `CreditMemoItemInterface` object") order_item: OrderItemInterface @doc(description: "The order item the credit memo is applied to") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItem") product_name: String @doc(description: "The name of the base product") product_sku: String! @doc(description: "SKU of the base product") diff --git a/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php index 0ee2ee09cad57..02da921e032e0 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php +++ b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php @@ -8,9 +8,9 @@ namespace Magento\SalesRule\Model\Coupon\Quote; use Magento\Quote\Api\Data\CartInterface; -use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; +use Magento\SalesRule\Model\Service\CouponUsagePublisher; /** * Updates the coupon usages from quote @@ -18,24 +18,24 @@ class UpdateCouponUsages { /** - * @var CouponUsageProcessor + * @var UpdateInfoFactory */ - private $couponUsageProcessor; + private $updateInfoFactory; /** - * @var UpdateInfoFactory + * @var CouponUsagePublisher */ - private $updateInfoFactory; + private $couponUsagePublisher; /** - * @param CouponUsageProcessor $couponUsageProcessor + * @param CouponUsagePublisher $couponUsagePublisher * @param UpdateInfoFactory $updateInfoFactory */ public function __construct( - CouponUsageProcessor $couponUsageProcessor, + CouponUsagePublisher $couponUsagePublisher, UpdateInfoFactory $updateInfoFactory ) { - $this->couponUsageProcessor = $couponUsageProcessor; + $this->couponUsagePublisher = $couponUsagePublisher; $this->updateInfoFactory = $updateInfoFactory; } @@ -59,6 +59,6 @@ public function execute(CartInterface $quote, bool $increment): void $updateInfo->setCustomerId((int)$quote->getCustomerId()); $updateInfo->setIsIncrement($increment); - $this->couponUsageProcessor->process($updateInfo); + $this->couponUsagePublisher->publish($updateInfo); } } diff --git a/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php b/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php new file mode 100644 index 0000000000000..0520cb658e408 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model; + +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; +use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\Exception\NotFoundException; +use Psr\Log\LoggerInterface; + +/** + * Consumer for coupon usage update + */ +class CouponUsageConsumer +{ + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var CouponUsageProcessor + */ + private $processor; + + /** + * @var EntityManager + */ + private $entityManager; + + /** + * @var UpdateInfoFactory + */ + private $updateInfoFactory; + + /** + * @param UpdateInfoFactory $updateInfoFactory + * @param CouponUsageProcessor $processor + * @param LoggerInterface $logger + * @param SerializerInterface $serializer + * @param EntityManager $entityManager + */ + public function __construct( + UpdateInfoFactory $updateInfoFactory, + CouponUsageProcessor $processor, + LoggerInterface $logger, + SerializerInterface $serializer, + EntityManager $entityManager + ) { + $this->updateInfoFactory = $updateInfoFactory; + $this->processor = $processor; + $this->logger = $logger; + $this->serializer = $serializer; + $this->entityManager = $entityManager; + } + + /** + * Process coupon usage update + * + * @param OperationInterface $operation + * @return void + * @throws \Exception + */ + public function process(OperationInterface $operation): void + { + try { + $serializedData = $operation->getSerializedData(); + $data = $this->serializer->unserialize($serializedData); + $updateInfo = $this->updateInfoFactory->create(); + $updateInfo->setData($data); + $this->processor->process($updateInfo); + } catch (NotFoundException $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = $e->getMessage(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED; + $errorCode = $e->getCode(); + $message = __('Sorry, something went wrong during rule usage update. Please see log for details.'); + } + + $operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE) + ->setErrorCode($errorCode ?? null) + ->setResultMessage($message ?? null); + + $this->entityManager->save($operation); + } +} diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule.php b/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule.php index fb3f420a325e6..907d2c0494364 100644 --- a/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule.php +++ b/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule.php @@ -90,8 +90,9 @@ public function aggregate($from = null, $to = null) */ public function getUniqRulesNamesList() { - $connection = $this->getConnection(); - $tableName = $this->getTable('salesrule_coupon_aggregated'); + $resourceModel = $this->_createdatFactory->create(); + $connection = $resourceModel->getConnection(); + $tableName = $resourceModel->getMainTable(); $select = $connection->select()->from( $tableName, new \Zend_Db_Expr('DISTINCT rule_name') diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php index 1569c9551aa46..0adeedc32f759 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -111,7 +111,7 @@ public function calculate($rule, $item, $qty) $address, $baseRuleTotals ) : $baseRuleTotals; - $availableDiscountAmount = $this->cartFixedDiscountHelper + $maximumItemDiscount = $this->cartFixedDiscountHelper ->getDiscountAmount( $ruleDiscount, $qty, @@ -119,8 +119,8 @@ public function calculate($rule, $item, $qty) $baseRuleTotals, $discountType ); - $quoteAmount = $this->priceCurrency->convert($availableDiscountAmount, $store); - $baseDiscountAmount = min($baseItemPrice * $qty, $availableDiscountAmount); + $quoteAmount = $this->priceCurrency->convert($maximumItemDiscount, $store); + $baseDiscountAmount = min($baseItemPrice * $qty, $maximumItemDiscount); $this->deltaPriceRound->reset($discountType); } else { $baseRuleTotals = $shippingMethod ? diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php index cf6301cb31a9c..62c1cc086048b 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Address.php @@ -62,6 +62,7 @@ public function loadAttributeOptions() { $attributes = [ 'base_subtotal_with_discount' => __('Subtotal (Excl. Tax)'), + 'base_subtotal_total_incl_tax' => __('Subtotal (Incl. Tax)'), 'base_subtotal' => __('Subtotal'), 'total_qty' => __('Total Items Quantity'), 'weight' => __('Total Weight'), @@ -99,6 +100,7 @@ public function getInputType() { switch ($this->getAttribute()) { case 'base_subtotal': + case 'base_subtotal_total_incl_tax': case 'weight': case 'total_qty': return 'numeric'; diff --git a/app/code/Magento/SalesRule/Model/Service/CouponUsagePublisher.php b/app/code/Magento/SalesRule/Model/Service/CouponUsagePublisher.php new file mode 100644 index 0000000000000..1d1bbb1f63ed3 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Service/CouponUsagePublisher.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Service; + +use Magento\Framework\Bulk\BulkManagementInterface; +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\Framework\DataObject\IdentityGeneratorInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\Bulk\OperationInterface; +use Magento\Authorization\Model\UserContextInterface; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; + +/** + * Scheduler for coupon usage queue + */ +class CouponUsagePublisher +{ + private const TOPIC_NAME = 'sales.rule.update.coupon.usage'; + + /** + * @var BulkManagementInterface + */ + private $bulkManagement; + + /** + * @var OperationInterfaceFactory + */ + private $operationFactory; + + /** + * @var IdentityGeneratorInterface + */ + private $identityService; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @param BulkManagementInterface $bulkManagement + * @param OperationInterfaceFactory $operartionFactory + * @param IdentityGeneratorInterface $identityService + * @param SerializerInterface $serializer + * @param UserContextInterface $userContext + */ + public function __construct( + BulkManagementInterface $bulkManagement, + OperationInterfaceFactory $operartionFactory, + IdentityGeneratorInterface $identityService, + SerializerInterface $serializer, + UserContextInterface $userContext + ) { + $this->bulkManagement = $bulkManagement; + $this->operationFactory = $operartionFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; + $this->userContext = $userContext; + } + + /** + * Publish sales rule usage info into the queue + * + * @param string $updateInfo + * @return boolean + */ + public function publish(UpdateInfo $updateInfo): bool + { + $bulkUuid = $this->identityService->generateId(); + $bulkDescription = __('Rule processing: %1', implode(',', $updateInfo->getAppliedRuleIds())); + + $data = [ + 'data' => [ + 'bulk_uuid' => $bulkUuid, + 'topic_name' => self::TOPIC_NAME, + 'serialized_data' => $this->serializer->serialize($updateInfo->getData()), + 'status' => OperationInterface::STATUS_TYPE_OPEN, + ] + ]; + $operation = $this->operationFactory->create($data); + + return $this->bulkManagement->scheduleBulk( + $bulkUuid, + [$operation], + $bulkDescription, + $this->userContext->getUserId() + ); + } +} diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index cc0333480f7b0..7b3a6b15b7a32 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -318,7 +318,7 @@ public function process(AbstractItem $item) public function processShippingAmount(Address $address) { $shippingAmount = $address->getShippingAmountForDiscount(); - if ($shippingAmount !== null) { + if (!empty($shippingAmount)) { $baseShippingAmount = $address->getBaseShippingAmountForDiscount(); } else { $shippingAmount = $address->getShippingAmount(); diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml index 57a2c17419532..f6a705ee368e9 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCreateCartPriceRuleActionGroup.xml @@ -19,6 +19,7 @@ <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="amOnCartPriceList"/> <waitForPageLoad stepKey="waitForPriceList"/> <click selector="{{AdminCartPriceRulesSection.addNewRuleButton}}" stepKey="clickAddNewRule"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.ruleName}}" stepKey="waitRuleNameFieldAppeared"/> <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{ruleName.name}}" stepKey="fillRuleName"/> <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" userInput="{{ruleName.websites}}" stepKey="selectWebsites"/> <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" parameterArray="[{{ruleName.customerGroups}}]" stepKey="selectCustomerGroup"/> @@ -26,6 +27,7 @@ <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="{{ruleName.apply}}" stepKey="selectActionType"/> <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="{{ruleName.discountAmount}}" stepKey="fillDiscountAmount"/> <click selector="{{AdminCartPriceRulesFormSection.save}}" stepKey="clickSaveButton"/> - <see selector="{{AdminCartPriceRulesFormSection.successMessage}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="seeSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml index c8363a3df6221..5607512c862b3 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/StorefrontApplyDiscountCodeActionGroup.xml @@ -12,7 +12,7 @@ <arguments> <argument name="discountCode" type="string"/> </arguments> - <click selector="{{DiscountSection.DiscountTab}}" stepKey="clickToAddDiscount"/> + <conditionalClick selector="{{DiscountSection.DiscountTab}}" dependentSelector="{{DiscountSection.CouponInput}}" visible="false" stepKey="clickToAddDiscount"/> <fillField selector="{{DiscountSection.DiscountInput}}" userInput="{{discountCode}}" stepKey="fillFieldDiscountCode"/> <click selector="{{DiscountSection.ApplyCodeBtn}}" stepKey="clickToApplyDiscount"/> <waitForElementVisible selector="{{DiscountSection.DiscountVerificationMsg}}" time="30" stepKey="waitForDiscountToBeAdded"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml index cc695b347c4fb..727222213b118 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleAddressConditionsData.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> <entity name="SalesRuleAddressConditions" type="SalesRuleConditionAttribute"> <data key="subtotal">Magento\SalesRule\Model\Rule\Condition\Address|base_subtotal</data> + <data key="base_subtotal_total_incl_tax">Magento\SalesRule\Model\Rule\Condition\Address|base_subtotal_total_incl_tax</data> <data key="totalItemsQty">Magento\SalesRule\Model\Rule\Condition\Address|total_qty</data> <data key="totalWeight">Magento\SalesRule\Model\Rule\Condition\Address|weight</data> <data key="shippingMethod">Magento\SalesRule\Model\Rule\Condition\Address|shipping_method</data> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml index 4a39e1237841d..4c1558baa767d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleData.xml @@ -261,6 +261,9 @@ <entity name="SalesRuleNoCouponWithFixedDiscount" extends="ApiCartRule"> <data key="simple_action">by_fixed</data> </entity> + <entity name="SalesRuleNoCouponWithFixedDiscountWholeCart" extends="ApiCartRule"> + <data key="simple_action">cart_fixed</data> + </entity> <entity name="ActiveSalesRuleWithPercentPriceDiscountCoupon"> <data key="name" unique="suffix">Cart Price Rule with Specific Coupon</data> <data key="description">Description for Cart Price Rule</data> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleQueueData.xml b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleQueueData.xml new file mode 100644 index 0000000000000..5ee98b49bd007 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Data/SalesRuleQueueData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="SalesRuleConsumerData"> + <data key="consumerName">sales.rule.update.coupon.usage</data> + <data key="messageLimit">10</data> + </entity> +</entities> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithSubtotalInclTaxTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithSubtotalInclTaxTest.xml new file mode 100644 index 0000000000000..a52e8e10459e5 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithSubtotalInclTaxTest.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCreateCartPriceRuleWithSubtotalInclTaxTest"> + <annotations> + <stories value="Create Sales Rule"/> + <title value="Create Cart Price Rule with Subtotal Incl Tax"/> + <description value="Test that cart price rule with Subtotal Incl Tax works correctly"/> + <severity value="CRITICAL"/> + <useCaseId value="MC-37729"/> + <testCaseId value="MC-38971"/> + <group value="SalesRule"/> + </annotations> + <before> + <!--Login to backend--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Create tax rate for US-CA-*--> + <createData entity="defaultTaxRate" stepKey="taxRate"/> + <!--Create tax rule--> + <actionGroup ref="AdminCreateTaxRuleActionGroup" stepKey="createTaxRule"> + <argument name="taxRate" value="$$taxRate$$"/> + <argument name="taxRule" value="SimpleTaxRule"/> + </actionGroup> + <!--Create simple product--> + <createData entity="SimpleProduct2" stepKey="product"> + <field key="price">100</field> + </createData> + <!--Create cart price rule with no coupon and 50% discount--> + <createData entity="ApiCartRule" stepKey="createCartPriceRule"/> + <!--Add "subtotal incl tax > 100" condition to cart price rule--> + <amOnPage url="{{AdminCartPriceRuleEditPage.url($$createCartPriceRule.rule_id$$)}}" stepKey="openEditRule"/> + <actionGroup ref="SetCartAttributeConditionForCartPriceRuleActionGroup" stepKey="setCartAttributeConditionForCartPriceRule"> + <argument name="attributeName" value="{{SalesRuleAddressConditions.base_subtotal_total_incl_tax}}"/> + <argument name="operatorType" value="greater than"/> + <argument name="value" value="100"/> + </actionGroup> + </before> + <after> + <!--Delete tax rule--> + <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> + <argument name="taxRuleCode" value="{{SimpleTaxRule.code}}" /> + </actionGroup> + <!--Delete tax rate--> + <deleteData createDataKey="taxRate" stepKey="deleteTaxRate"/> + <!--Delete product--> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <!--Delete cart price rule--> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Open product --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct2Page"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <!--Add to cart --> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product2AddToCart"/> + <!--Click on mini cart--> + <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> + <!--Click on view and edit cart link--> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <waitForPageLoad stepKey="waitForViewAndEditCartToOpen"/> + <!--Assert that tax and discount are not applied by default--> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="AssertTaxAndDiscountIsNotApplied"> + <argument name="subtotal" value="$100.00"/> + <argument name="shipping" value="$5.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="$105.00"/> + </actionGroup> + <dontSee selector="{{CheckoutCartSummarySection.discountAmount}}" stepKey="assertDiscountIsNotApplied"/> + <!--Open "Estimate Shipping and Tax" section and fill US-CA address --> + <actionGroup ref="StorefrontCartEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxSection"> + <argument name="estimateAddress" value="EstimateAddressCalifornia"/> + </actionGroup> + <!--Assert that tax and discount are applied by to total amount--> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="AssertTaxAndDiscountIsApplied"> + <argument name="subtotal" value="$100.00"/> + <argument name="shipping" value="$5.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="$60.00"/> + </actionGroup> + <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$50.00" stepKey="assertDiscountIsApplied"/> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 7a995b1feeeda..d76e9edb828bd 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -10,6 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="EndToEndB2CLoggedInUserTest"> <before> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" after="loginAsAdmin" stepKey="deleteCartPriceRule"/> <createData entity="ApiSalesRule" stepKey="createSalesRule"/> <createData entity="ApiSalesRuleCoupon" stepKey="createSalesRuleCoupon"> <requiredEntity createDataKey="createSalesRule"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAssertFixedCartDiscountAmountForBundleProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAssertFixedCartDiscountAmountForBundleProductTest.xml new file mode 100644 index 0000000000000..42e6d9e1d5b09 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAssertFixedCartDiscountAmountForBundleProductTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontAssertFixedCartDiscountAmountForBundleProductTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Fixed Amount Cart Price Rule"/> + <title value="Checking Fixed Amount Cart Price Rule is correctly applied to bundle products"/> + <description value="Checking Fixed Amount Cart Price Rule is correctly applied to bundle products"/> + <severity value="AVERAGE"/> + <group value="SalesRule"/> + <testCaseId value="MC-39480"/> + </annotations> + <before> + <createData entity="SalesRuleNoCouponWithFixedDiscountWholeCart" stepKey="createSalesRule"/> + <actionGroup ref="AdminCreateApiDynamicBundleProductAllOptionTypesActionGroup" stepKey="createBundleProduct"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCatalogInventory"> + <argument name="indices" value="cataloginventory_stock"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + <deleteData createDataKey="createBundleProductCreateBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1CreateBundleProduct" stepKey="deleteSimpleProduct1"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createBundleProductCreateBundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + <selectOption selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="selectOption0Product1"/> + <seeOptionIsSelected selector="{{StorefrontBundledSection.dropDownOptionOneProducts('Drop-down Option')}}" userInput="$$simpleProduct2CreateBundleProduct.sku$$ +$$$simpleProduct2CreateBundleProduct.price$$.00" stepKey="checkOption0Product1"/> + <checkOption selector="{{StorefrontBundledSection.radioButtonOptionTwoProducts('Radio Buttons Option', '1')}}" stepKey="selectOption1Product0"/> + <seeCheckboxIsChecked selector="{{StorefrontBundledSection.radioButtonOptionTwoProducts('Radio Buttons Option', '1')}}" stepKey="checkOption1Product0"/> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts('Checkbox Option', '1')}}" stepKey="selectOption2Product0"/> + <seeCheckboxIsChecked selector="{{StorefrontBundledSection.checkboxOptionThreeProducts('Checkbox Option', '1')}}" stepKey="checkOption2Product0"/> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts('Checkbox Option', '2')}}" stepKey="selectOption2Product1"/> + <seeCheckboxIsChecked selector="{{StorefrontBundledSection.checkboxOptionThreeProducts('Checkbox Option', '1')}}" stepKey="checkOption2Product1"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + <actionGroup ref="StorefrontCheckCartActionGroup" stepKey="cartAssert" after="openShoppingCartPage"> + <argument name="subtotal" value="60.00"/> + <argument name="shipping" value="5.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="15.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index 159f2a1a82ece..18de326a2919d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -102,6 +102,12 @@ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage"/> + <!-- Start the usage processing consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startUsageProcessingMessageQueue1"> + <argument name="consumerName" value="{{SalesRuleConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{SalesRuleConsumerData.messageLimit}}"/> + </actionGroup> + <!-- Step: 9-10. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage1"/> <waitForPageLoad stepKey="waitForPageLoad3"/> @@ -142,6 +148,12 @@ <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder1"/> <waitForElement selector="{{CheckoutSuccessMainSection.success}}" time="30" stepKey="waitForLoadSuccessPage1"/> + <!-- Start the usage processing consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startUsageProcessingMessageQueue2"> + <argument name="consumerName" value="{{SalesRuleConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{SalesRuleConsumerData.messageLimit}}"/> + </actionGroup> + <!-- Step; 15-16. Open the Product Page, add the product to Cart, go to Shopping Cart and Apply the same coupon code --> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="openProductPage3"/> <waitForPageLoad stepKey="waitForPageLoad5"/> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/RuleTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/RuleTest.php index 1a302747fe454..9cb9767f47932 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/RuleTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ResourceModel/Report/RuleTest.php @@ -7,13 +7,13 @@ namespace Magento\SalesRule\Test\Unit\Model\ResourceModel\Report; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\DB\Select; use Magento\Framework\DB\Select\SelectRenderer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Reports\Model\FlagFactory; use Magento\SalesRule\Model\ResourceModel\Report\Rule; +use Magento\SalesRule\Model\ResourceModel\Report\Rule\Createdat; use Magento\SalesRule\Model\ResourceModel\Report\Rule\CreatedatFactory; use Magento\SalesRule\Model\ResourceModel\Report\Rule\UpdatedatFactory; use PHPUnit\Framework\TestCase; @@ -84,14 +84,20 @@ function ($value) { [$this, 'fetchAllCallback'] ); - $resourceMock = $this->createMock(ResourceConnection::class); - $resourceMock->expects($this->any())->method('getConnection')->willReturn($connectionMock); - $resourceMock->expects($this->once())->method('getTableName')->willReturn(self::TABLE_NAME); - $flagFactory = $this->createMock(FlagFactory::class); - $createdatFactoryMock = $this->createPartialMock( + + $createdatResourceModel = $this->createConfiguredMock( + Createdat::class, + [ + 'getConnection' => $connectionMock, + 'getMainTable' => self::TABLE_NAME, + ] + ); + $createdatFactoryMock = $this->createConfiguredMock( CreatedatFactory::class, - ['create'] + [ + 'create' => $createdatResourceModel + ] ); $updatedatFactoryMock = $this->createPartialMock( UpdatedatFactory::class, @@ -102,7 +108,6 @@ function ($value) { $model = $objectHelper->getObject( Rule::class, [ - 'resource' => $resourceMock, 'reportsFlagFactory' => $flagFactory, 'createdatFactory' => $createdatFactoryMock, 'updatedatFactory' => $updatedatFactoryMock diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/AddressTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/AddressTest.php new file mode 100644 index 0000000000000..70036c06922c5 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Condition/AddressTest.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Test\Unit\Model\Rule\Condition; + +use Magento\SalesRule\Model\Rule\Condition\Address; +use PHPUnit\Framework\TestCase; + +/** + * Test for address rule condition + */ +class AddressTest extends TestCase +{ + /** + * @var Address + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $context = $this->createMock(\Magento\Rule\Model\Condition\Context::class); + $directoryCountry = $this->createMock(\Magento\Directory\Model\Config\Source\Country::class); + $directoryAllregion = $this->createMock(\Magento\Directory\Model\Config\Source\Allregion::class); + $shippingAllmethods = $this->createMock(\Magento\Shipping\Model\Config\Source\Allmethods::class); + $paymentAllmethods = $this->createMock(\Magento\Payment\Model\Config\Source\Allmethods::class); + $this->model = new Address( + $context, + $directoryCountry, + $directoryAllregion, + $shippingAllmethods, + $paymentAllmethods + ); + } + + /** + * Test that all attributes are present in options list + */ + public function testLoadAttributeOptions(): void + { + $attributes = [ + 'base_subtotal_with_discount', + 'base_subtotal_total_incl_tax', + 'base_subtotal', + 'total_qty', + 'weight', + 'payment_method', + 'shipping_method', + 'postcode', + 'region', + 'region_id', + 'country_id', + ]; + + $this->model->loadAttributeOptions(); + $this->assertEquals($attributes, array_keys($this->model->getAttributeOption())); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php index 4224cfafb3c8c..c895802153f45 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/ValidatorTest.php @@ -33,6 +33,7 @@ use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Zend_Db_Select_Exception; /** * Test sales rule model validator @@ -538,7 +539,7 @@ public function testProcessShippingAmountProcessDisabled() * @param int $ruleDiscount * @param int $shippingDiscount * @dataProvider dataProviderActions - * @throws \Zend_Db_Select_Exception + * @throws Zend_Db_Select_Exception */ public function testProcessShippingAmountActions($action, $ruleDiscount, $shippingDiscount): void { @@ -595,6 +596,86 @@ public static function dataProviderActions() ]; } + /** + * Tests shipping amount with full discount action. + * + * @dataProvider dataProviderForFullShippingDiscount + * @param string $action + * @param float $ruleDiscount + * @param float $shippingDiscount + * @param float $shippingAmount + * @param float $quoteBaseSubTotal + * @throws Zend_Db_Select_Exception + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testProcessShippingAmountWithFullFixedPercentDiscount( + string $action, + float $ruleDiscount, + float $shippingDiscount, + float $shippingAmount, + float $quoteBaseSubTotal + ): void { + $ruleMock = $this->getMockBuilder(Rule::class) + ->disableOriginalConstructor() + ->setMethods(['getApplyToShipping', 'getSimpleAction', 'getDiscountAmount']) + ->getMock(); + $ruleMock->method('getApplyToShipping') + ->willReturn(true); + $ruleMock->method('getDiscountAmount') + ->willReturn($ruleDiscount); + $ruleMock->method('getSimpleAction') + ->willReturn($action); + + $iterator = new \ArrayIterator([$ruleMock]); + $this->ruleCollection->method('getIterator') + ->willReturn($iterator); + + $this->utility->method('canProcessRule') + ->willReturn(true); + + $this->priceCurrency->method('convert') + ->willReturn($ruleDiscount); + + $this->priceCurrency->method('roundPrice') + ->willReturn(round($shippingDiscount, 2)); + + $this->model->init( + $this->model->getWebsiteId(), + $this->model->getCustomerGroupId(), + $this->model->getCouponCode() + ); + + $addressMock = $this->setupAddressMock($shippingAmount, $quoteBaseSubTotal); + + self::assertInstanceOf(Validator::class, $this->model->processShippingAmount($addressMock)); + self::assertEquals($shippingDiscount, $addressMock->getShippingDiscountAmount()); + } + + /** + * Get data provider array for full shipping discount action + * + * @return array + */ + public function dataProviderForFullShippingDiscount(): array + { + return [ + 'verify shipping discount when shipping amount is greater than zero' => [ + Rule::BY_PERCENT_ACTION, + 100.00, + 5.0, + 5.0, + 10.0 + ], + 'verify shipping discount when shipping amount is zero' => [ + Rule::BY_PERCENT_ACTION, + 100.00, + 5.0, + 0, + 10.0 + ] + ]; + } + /** * @param float $shippingAmount * @param float $quoteBaseSubTotal diff --git a/app/code/Magento/SalesRule/composer.json b/app/code/Magento/SalesRule/composer.json index 572e191093275..20246f67e337e 100644 --- a/app/code/Magento/SalesRule/composer.json +++ b/app/code/Magento/SalesRule/composer.json @@ -7,6 +7,7 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/framework-bulk": "*", "magento/module-backend": "*", "magento/module-catalog": "*", "magento/module-catalog-rule": "*", @@ -25,7 +26,8 @@ "magento/module-widget": "*", "magento/module-captcha": "*", "magento/module-checkout": "*", - "magento/module-authorization": "*" + "magento/module-authorization": "*", + "magento/module-asynchronous-operations": "*" }, "suggest": { "magento/module-sales-rule-sample-data": "*" diff --git a/app/code/Magento/SalesRule/etc/communication.xml b/app/code/Magento/SalesRule/etc/communication.xml index 4c905fa83e2fd..786e866f0e3c5 100644 --- a/app/code/Magento/SalesRule/etc/communication.xml +++ b/app/code/Magento/SalesRule/etc/communication.xml @@ -9,4 +9,7 @@ <topic name="sales_rule.codegenerator" request="Magento\SalesRule\Api\Data\CouponGenerationSpecInterface"> <handler name="codegeneratorProcessor" type="Magento\SalesRule\Model\Coupon\Consumer" method="process" /> </topic> + <topic name="sales.rule.update.coupon.usage" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> + <handler name="sales.rule.update.coupon.usage" type="Magento\SalesRule\Model\CouponUsageConsumer" method="process" /> + </topic> </config> diff --git a/app/code/Magento/SalesRule/etc/queue.xml b/app/code/Magento/SalesRule/etc/queue.xml index 8217a0b9f6c1a..87dce71b53054 100644 --- a/app/code/Magento/SalesRule/etc/queue.xml +++ b/app/code/Magento/SalesRule/etc/queue.xml @@ -9,4 +9,7 @@ <broker topic="sales_rule.codegenerator" exchange="magento-db" type="db"> <queue name="codegenerator" consumer="codegeneratorProcessor" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\SalesRule\Model\Coupon\Consumer::process"/> </broker> + <broker topic="sales.rule.update.coupon.usage" exchange="magento-db" type="db"> + <queue name="sales.rule.update.coupon.usage" consumer="sales.rule.update.coupon.usage" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\SalesRule\Model\CouponUsageConsumer::process"/> + </broker> </config> diff --git a/app/code/Magento/SalesRule/etc/queue_consumer.xml b/app/code/Magento/SalesRule/etc/queue_consumer.xml index 9eb585f48e8e3..bcebaf6a543b9 100644 --- a/app/code/Magento/SalesRule/etc/queue_consumer.xml +++ b/app/code/Magento/SalesRule/etc/queue_consumer.xml @@ -7,4 +7,5 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> <consumer name="codegeneratorProcessor" queue="codegenerator" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\SalesRule\Model\Coupon\Consumer::process" /> + <consumer name="sales.rule.update.coupon.usage" queue="sales.rule.update.coupon.usage" connection="db" maxMessages="5000" consumerInstance="Magento\Framework\MessageQueue\Consumer" handler="Magento\SalesRule\Model\CouponUsageConsumer::process" /> </config> diff --git a/app/code/Magento/SalesRule/etc/queue_publisher.xml b/app/code/Magento/SalesRule/etc/queue_publisher.xml index 0863fba2307c5..f1b8bddf2c090 100644 --- a/app/code/Magento/SalesRule/etc/queue_publisher.xml +++ b/app/code/Magento/SalesRule/etc/queue_publisher.xml @@ -9,4 +9,7 @@ <publisher topic="sales_rule.codegenerator"> <connection name="db" exchange="magento-db" /> </publisher> + <publisher topic="sales.rule.update.coupon.usage"> + <connection name="db" exchange="magento-db" /> + </publisher> </config> diff --git a/app/code/Magento/SalesRule/etc/queue_topology.xml b/app/code/Magento/SalesRule/etc/queue_topology.xml index fd6a9bf36721c..3902c8a3ab36f 100644 --- a/app/code/Magento/SalesRule/etc/queue_topology.xml +++ b/app/code/Magento/SalesRule/etc/queue_topology.xml @@ -8,5 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> <exchange name="magento-db" type="topic" connection="db"> <binding id="codegeneratorBinding" topic="sales_rule.codegenerator" destinationType="queue" destination="codegenerator"/> + <binding id="couponUsageBinding" topic="sales.rule.update.coupon.usage" destinationType="queue" destination="sales.rule.update.coupon.usage"/> </exchange> </config> diff --git a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml index 34379fd6d2e4a..088bd5eed4cc3 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/StorefrontQuickSearchSection.xml @@ -14,5 +14,6 @@ <element name="searchMiniForm" type="input" selector="#search_mini_form"/> <element name="searchDropDownSuggestion" type="text" selector="//div[@id='search_autocomplete']/ul/li/span"/> <element name="searchDropDownName" type="text" selector="//div[@id='search_autocomplete']//span[contains(., '{{searchQuery}}')]" parameterized="true"/> + <element name="searchMagnifierIcon" type="text" selector="//*[@id='search_mini_form']//label[@data-role='minisearch-label']"/> </section> </sections> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchFieldVisibilityWhenSuggestionDisabledTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchFieldVisibilityWhenSuggestionDisabledTest.xml new file mode 100644 index 0000000000000..cdd52bd2b524e --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchFieldVisibilityWhenSuggestionDisabledTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySearchFieldVisibilityWhenSuggestionDisabledTest"> + <annotations> + <stories value="Search Term"/> + <title value="Mini search field appears if suggestions was disabled"/> + <description value="Mini search field appears if suggestions was disabled"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-39443"/> + <group value="mtf_migrated"/> + </annotations> + + <before> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!-- Disable search suggestion --> + <magentoCLI command="config:set catalog/search/search_suggestion_enabled 0" stepKey="disableSearchSuggestion"/> + + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheFirst"> + <argument name="tags" value="config full_page"/> + </actionGroup> + </before> + + <after> + <!-- Enable search suggestion back --> + <magentoCLI command="config:set catalog/search/search_suggestion_enabled 1" stepKey="disableSearchSuggestion"/> + + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCacheSecond"> + <argument name="tags" value="config full_page"/> + </actionGroup> + + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Go to storefront home page --> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> + + <resizeWindow width="767" height="720" stepKey="resizeWindowToMobileView"/> + + <click selector="{{StorefrontQuickSearchSection.searchMagnifierIcon}}" stepKey="clickOnMagnifierSearchIcon"/> + + <waitForElementVisible selector="{{StorefrontQuickSearchSection.searchPhrase}}" after="clickOnMagnifierSearchIcon" stepKey="seeInputSearchActive"/> + + </test> +</tests> diff --git a/app/code/Magento/Search/ViewModel/ConfigProvider.php b/app/code/Magento/Search/ViewModel/ConfigProvider.php index be3366e62e965..b1db5b57e13e0 100644 --- a/app/code/Magento/Search/ViewModel/ConfigProvider.php +++ b/app/code/Magento/Search/ViewModel/ConfigProvider.php @@ -10,6 +10,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\View\Element\Block\ArgumentInterface; use Magento\Store\Model\ScopeInterface; +use Magento\Search\Helper\Data as SearchHelper; /** * View model for search @@ -26,13 +27,31 @@ class ConfigProvider implements ArgumentInterface */ private $scopeConfig; + /** + * @var SearchHelper + */ + private $searchHelper; + /** * @param ScopeConfigInterface $scopeConfig + * @param SearchHelper $searchHelper */ public function __construct( - ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + SearchHelper $searchHelper ) { $this->scopeConfig = $scopeConfig; + $this->searchHelper = $searchHelper; + } + + /** + * Retrieve search helper instance for template view + * + * @return SearchHelper + */ + public function getSearchHelperData(): SearchHelper + { + return $this->searchHelper; } /** diff --git a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml index 80e720e2c2fe2..c6b2a6a729575 100644 --- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml +++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml @@ -4,40 +4,42 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php /** @var $block \Magento\Framework\View\Element\Template */ -/** @var $helper \Magento\Search\Helper\Data */ +/** @var $escaper \Magento\Framework\Escaper */ /** @var $configProvider \Magento\Search\ViewModel\ConfigProvider */ -$helper = $this->helper(\Magento\Search\Helper\Data::class); $configProvider = $block->getData('configProvider'); +/** @var $helper \Magento\Search\Helper\Data */ +$helper = $configProvider->getSearchHelperData(); +$allowedSuggestion = $configProvider->isSuggestionsAllowed(); +$quickSearchUrl = $allowedSuggestion ? $escaper->escapeUrl($helper->getSuggestUrl()) : ''; ?> <div class="block block-search"> - <div class="block block-title"><strong><?= $block->escapeHtml(__('Search')) ?></strong></div> + <div class="block block-title"><strong><?= $escaper->escapeHtml(__('Search')) ?></strong></div> <div class="block block-content"> <form class="form minisearch" id="search_mini_form" - action="<?= $block->escapeUrl($helper->getResultUrl()) ?>" method="get"> + action="<?= $escaper->escapeUrl($helper->getResultUrl()) ?>" method="get"> <div class="field search"> <label class="label" for="search" data-role="minisearch-label"> - <span><?= $block->escapeHtml(__('Search')) ?></span> + <span><?= $escaper->escapeHtml(__('Search')) ?></span> </label> <div class="control"> <input id="search" - <?php if ($configProvider->isSuggestionsAllowed()):?> - data-mage-init='{"quickSearch":{ - "formSelector":"#search_mini_form", - "url":"<?= $block->escapeUrl($helper->getSuggestUrl())?>", - "destinationSelector":"#search_autocomplete", - "minSearchLength":"<?= $block->escapeHtml($helper->getMinQueryLength()) ?>"} - }' - <?php endif;?> + data-mage-init='{ + "quickSearch": { + "formSelector": "#search_mini_form", + "url": "<?= /* @noEscape */ $quickSearchUrl ?>", + "destinationSelector": "#search_autocomplete", + "minSearchLength": "<?= $escaper->escapeHtml($helper->getMinQueryLength()) ?>" + } + }' type="text" - name="<?= $block->escapeHtmlAttr($helper->getQueryParamName()) ?>" + name="<?= $escaper->escapeHtmlAttr($helper->getQueryParamName()) ?>" value="<?= /* @noEscape */ $helper->getEscapedQueryText() ?>" - placeholder="<?= $block->escapeHtmlAttr(__('Search entire store here...')) ?>" + placeholder="<?= $escaper->escapeHtmlAttr(__('Search entire store here...')) ?>" class="input-text" - maxlength="<?= $block->escapeHtmlAttr($helper->getMaxQueryLength()) ?>" + maxlength="<?= $escaper->escapeHtmlAttr($helper->getMaxQueryLength()) ?>" role="combobox" aria-haspopup="false" aria-autocomplete="both" @@ -49,11 +51,11 @@ $configProvider = $block->getData('configProvider'); </div> <div class="actions"> <button type="submit" - title="<?= $block->escapeHtml(__('Search')) ?>" - class="action search" - aria-label="Search" + title="<?= $escaper->escapeHtml(__('Search')) ?>" + class="action search" + aria-label="Search" > - <span><?= $block->escapeHtml(__('Search')) ?></span> + <span><?= $escaper->escapeHtml(__('Search')) ?></span> </button> </div> </form> diff --git a/app/code/Magento/Search/view/frontend/web/js/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js index 9b4c814f73d73..b8034fead76d0 100644 --- a/app/code/Magento/Search/view/frontend/web/js/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/js/form-mini.js @@ -35,12 +35,12 @@ define([ selectClass: 'selected', template: '<li class="<%- data.row_class %>" id="qs-option-<%- data.index %>" role="option">' + - '<span class="qs-option-name">' + - ' <%- data.title %>' + - '</span>' + - '<span aria-hidden="true" class="amount">' + - '<%- data.num_results %>' + - '</span>' + + '<span class="qs-option-name">' + + ' <%- data.title %>' + + '</span>' + + '<span aria-hidden="true" class="amount">' + + '<%- data.num_results %>' + + '</span>' + '</li>', submitBtn: 'button[type="submit"]', searchLabel: '[data-role=minisearch-label]', @@ -300,60 +300,63 @@ define([ if (value.length >= parseInt(this.options.minSearchLength, 10)) { this.submitBtn.disabled = false; - $.getJSON(this.options.url, { - q: value - }, $.proxy(function (data) { - if (data.length) { - $.each(data, function (index, element) { - var html; - - element.index = index; - html = template({ - data: element - }); - dropdown.append(html); - }); - - this._resetResponseList(true); - this.responseList.indexList = this.autoComplete.html(dropdown) - .css(clonePosition) - .show() - .find(this.options.responseFieldElements + ':visible'); - - this.element.removeAttr('aria-activedescendant'); + if (this.options.url !== '') { //eslint-disable-line eqeqeq + $.getJSON(this.options.url, { + q: value + }, $.proxy(function (data) { + if (data.length) { + $.each(data, function (index, element) { + var html; + + element.index = index; + html = template({ + data: element + }); + dropdown.append(html); + }); - if (this.responseList.indexList.length) { - this._updateAriaHasPopup(true); + this._resetResponseList(true); + + this.responseList.indexList = this.autoComplete.html(dropdown) + .css(clonePosition) + .show() + .find(this.options.responseFieldElements + ':visible'); + + this.element.removeAttr('aria-activedescendant'); + + if (this.responseList.indexList.length) { + this._updateAriaHasPopup(true); + } else { + this._updateAriaHasPopup(false); + } + + this.responseList.indexList + .on('click', function (e) { + this.responseList.selected = $(e.currentTarget); + this.searchForm.trigger('submit'); + }.bind(this)) + .on('mouseenter mouseleave', function (e) { + this.responseList.indexList.removeClass(this.options.selectClass); + $(e.target).addClass(this.options.selectClass); + this.responseList.selected = $(e.target); + this.element.attr('aria-activedescendant', $(e.target).attr('id')); + }.bind(this)) + .on('mouseout', function (e) { + if (!this._getLastElement() && + this._getLastElement().hasClass(this.options.selectClass)) { + $(e.target).removeClass(this.options.selectClass); + this._resetResponseList(false); + } + }.bind(this)); } else { + this._resetResponseList(true); + this.autoComplete.hide(); this._updateAriaHasPopup(false); + this.element.removeAttr('aria-activedescendant'); } - - this.responseList.indexList - .on('click', function (e) { - this.responseList.selected = $(e.currentTarget); - this.searchForm.trigger('submit'); - }.bind(this)) - .on('mouseenter mouseleave', function (e) { - this.responseList.indexList.removeClass(this.options.selectClass); - $(e.target).addClass(this.options.selectClass); - this.responseList.selected = $(e.target); - this.element.attr('aria-activedescendant', $(e.target).attr('id')); - }.bind(this)) - .on('mouseout', function (e) { - if (!this._getLastElement() && - this._getLastElement().hasClass(this.options.selectClass)) { - $(e.target).removeClass(this.options.selectClass); - this._resetResponseList(false); - } - }.bind(this)); - } else { - this._resetResponseList(true); - this.autoComplete.hide(); - this._updateAriaHasPopup(false); - this.element.removeAttr('aria-activedescendant'); - } - }, this)); + }, this)); + } } else { this._resetResponseList(true); this.autoComplete.hide(); diff --git a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php index 100ba029beabd..0c9738540322c 100644 --- a/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php +++ b/app/code/Magento/Shipping/Controller/Adminhtml/Order/Shipment/Save.php @@ -8,10 +8,11 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultFactory; +use Magento\Sales\Helper\Data as SalesData; use Magento\Sales\Model\Order\Shipment\Validation\QuantityValidator; /** - * Class Save + * Controller for generation of new Shipments from Backend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterface @@ -43,19 +44,26 @@ class Save extends \Magento\Backend\App\Action implements HttpPostActionInterfac */ private $shipmentValidator; + /** + * @var SalesData + */ + private $salesData; + /** * @param \Magento\Backend\App\Action\Context $context * @param \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader * @param \Magento\Shipping\Model\Shipping\LabelGenerator $labelGenerator * @param \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender * @param \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface|null $shipmentValidator + * @param SalesData $salesData */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader $shipmentLoader, \Magento\Shipping\Model\Shipping\LabelGenerator $labelGenerator, \Magento\Sales\Model\Order\Email\Sender\ShipmentSender $shipmentSender, - \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface $shipmentValidator = null + \Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface $shipmentValidator = null, + SalesData $salesData = null ) { parent::__construct($context); @@ -64,6 +72,8 @@ public function __construct( $this->shipmentSender = $shipmentSender; $this->shipmentValidator = $shipmentValidator ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Sales\Model\Order\Shipment\ShipmentValidatorInterface::class); + $this->salesData = $salesData ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(SalesData::class); } /** @@ -109,6 +119,7 @@ public function execute() } $data = $this->getRequest()->getParam('shipment'); + $orderId = $this->getRequest()->getParam('order_id'); if (!empty($data['comment_text'])) { $this->_objectManager->get(\Magento\Backend\Model\Session::class)->setCommentText($data['comment_text']); @@ -118,7 +129,7 @@ public function execute() $responseAjax = new \Magento\Framework\DataObject(); try { - $this->shipmentLoader->setOrderId($this->getRequest()->getParam('order_id')); + $this->shipmentLoader->setOrderId($orderId); $this->shipmentLoader->setShipmentId($this->getRequest()->getParam('shipment_id')); $this->shipmentLoader->setShipment($data); $this->shipmentLoader->setTracking($this->getRequest()->getParam('tracking')); @@ -143,7 +154,7 @@ public function execute() $this->messageManager->addErrorMessage( __("Shipment Document Validation Error(s):\n" . implode("\n", $validationResult->getMessages())) ); - return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + return $resultRedirect->setPath('*/*/new', ['order_id' => $orderId]); } $shipment->register(); @@ -156,7 +167,7 @@ public function execute() $this->_saveShipment($shipment); - if (!empty($data['send_email'])) { + if (!empty($data['send_email']) && $this->salesData->canSendNewShipmentEmail()) { $this->shipmentSender->send($shipment); } @@ -173,7 +184,7 @@ public function execute() $responseAjax->setMessage($e->getMessage()); } else { $this->messageManager->addErrorMessage($e->getMessage()); - return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + return $resultRedirect->setPath('*/*/new', ['order_id' => $orderId]); } } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); @@ -182,13 +193,13 @@ public function execute() $responseAjax->setMessage(__('An error occurred while creating shipping label.')); } else { $this->messageManager->addErrorMessage(__('Cannot save shipment.')); - return $resultRedirect->setPath('*/*/new', ['order_id' => $this->getRequest()->getParam('order_id')]); + return $resultRedirect->setPath('*/*/new', ['order_id' => $orderId]); } } if ($isNeedCreateLabel) { return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setJsonData($responseAjax->toJson()); } - return $resultRedirect->setPath('sales/order/view', ['order_id' => $shipment->getOrderId()]); + return $resultRedirect->setPath('sales/order/view', ['order_id' => $orderId]); } } diff --git a/app/code/Magento/Shipping/Model/Config.php b/app/code/Magento/Shipping/Model/Config.php index 26a76c90d3c85..565901ebe8ef0 100644 --- a/app/code/Magento/Shipping/Model/Config.php +++ b/app/code/Magento/Shipping/Model/Config.php @@ -4,16 +4,21 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Shipping\Model; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; use Magento\Shipping\Model\Carrier\AbstractCarrierInterface; +use Magento\Store\Model\ScopeInterface; /** - * Class Config + * Config model for shipping * @api * @since 100.0.2 */ -class Config extends \Magento\Framework\DataObject +class Config extends DataObject { /** * Shipping origin settings @@ -29,25 +34,25 @@ class Config extends \Magento\Framework\DataObject /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $_scopeConfig; /** - * @var \Magento\Shipping\Model\CarrierFactory + * @var CarrierFactory */ protected $_carrierFactory; /** * Constructor * - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory + * @param ScopeConfigInterface $scopeConfig + * @param CarrierFactory $carrierFactory * @param array $data */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Shipping\Model\CarrierFactory $carrierFactory, + ScopeConfigInterface $scopeConfig, + CarrierFactory $carrierFactory, array $data = [] ) { $this->_scopeConfig = $scopeConfig; @@ -58,17 +63,17 @@ public function __construct( /** * Retrieve active system carriers * - * @param mixed $store - * @return AbstractCarrierInterface[] + * @param mixed $store + * @return AbstractCarrierInterface[] */ public function getActiveCarriers($store = null) { $carriers = []; - $config = $this->_scopeConfig->getValue('carriers', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + $config = $this->getCarriersConfig($store); foreach (array_keys($config) as $carrierCode) { if ($this->_scopeConfig->isSetFlag( 'carriers/' . $carrierCode . '/active', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + ScopeInterface::SCOPE_STORE, $store )) { $carrierModel = $this->_carrierFactory->create($carrierCode, $store); @@ -77,25 +82,38 @@ public function getActiveCarriers($store = null) } } } + return $carriers; } /** * Retrieve all system carriers * - * @param mixed $store - * @return AbstractCarrierInterface[] + * @param mixed $store + * @return AbstractCarrierInterface[] */ public function getAllCarriers($store = null) { $carriers = []; - $config = $this->_scopeConfig->getValue('carriers', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $store); + $config = $this->getCarriersConfig($store); foreach (array_keys($config) as $carrierCode) { $model = $this->_carrierFactory->create($carrierCode, $store); if ($model) { $carriers[$carrierCode] = $model; } } + return $carriers; } + + /** + * Returns carriers config by store + * + * @param mixed $store + * @return array + */ + private function getCarriersConfig($store = null): array + { + return $this->_scopeConfig->getValue('carriers', ScopeInterface::SCOPE_STORE, $store) ?: []; + } } diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml index d448f51a00406..c174517375779 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml @@ -27,6 +27,8 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> + <!-- Customer Log Out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> diff --git a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php index 1d71f8a0d8e0c..afd5f8819a8e0 100644 --- a/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php +++ b/app/code/Magento/Shipping/Test/Unit/Controller/Adminhtml/Order/Shipment/SaveTest.php @@ -21,6 +21,7 @@ use Magento\Framework\Message\Manager; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Helper\Data as SalesData; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Sender\ShipmentSender; use Magento\Sales\Model\Order\Shipment; @@ -119,6 +120,11 @@ class SaveTest extends TestCase */ private $validationResult; + /** + * @var SalesData|MockObject + */ + private $salesData; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -140,7 +146,14 @@ protected function setUp(): void ->getMock(); $this->shipmentSender = $this->getMockBuilder(ShipmentSender::class) ->disableOriginalConstructor() - ->setMethods([]) + ->setMethods(['send']) + ->getMock(); + $this->shipmentSender->expects($this->any()) + ->method('send') + ->willReturn(true); + $this->salesData = $this->getMockBuilder(SalesData::class) + ->disableOriginalConstructor() + ->setMethods(['canSendNewShipmentEmail']) ->getMock(); $this->objectManager = $this->getMockForAbstractClass(ObjectManagerInterface::class); $this->context = $this->createPartialMock(Context::class, [ @@ -232,7 +245,8 @@ protected function setUp(): void 'shipmentLoader' => $this->shipmentLoader, 'request' => $this->request, 'response' => $this->response, - 'shipmentValidator' => $this->shipmentValidatorMock + 'shipmentValidator' => $this->shipmentValidatorMock, + 'salesData' => $this->salesData ] ); } @@ -240,11 +254,19 @@ protected function setUp(): void /** * @param bool $formKeyIsValid * @param bool $isPost + * @param string $sendEmail + * @param bool $emailEnabled + * @param bool $shouldEmailBeSent * @dataProvider executeDataProvider * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testExecute($formKeyIsValid, $isPost) - { + public function testExecute( + $formKeyIsValid, + $isPost, + $sendEmail, + $emailEnabled, + $shouldEmailBeSent + ) { $this->formKeyValidator->expects($this->any()) ->method('validate') ->willReturn($formKeyIsValid); @@ -269,7 +291,7 @@ public function testExecute($formKeyIsValid, $isPost) $shipmentId = 1000012; $orderId = 10003; $tracking = []; - $shipmentData = ['items' => [], 'send_email' => '']; + $shipmentData = ['items' => [], 'send_email' => $sendEmail]; $shipment = $this->createPartialMock( Shipment::class, ['load', 'save', 'register', 'getOrder', 'getOrderId', '__wakeup'] @@ -287,6 +309,13 @@ public function testExecute($formKeyIsValid, $isPost) ] ); + $this->salesData->expects($this->any()) + ->method('canSendNewShipmentEmail') + ->willReturn($emailEnabled); + if ($shouldEmailBeSent) { + $this->shipmentSender->expects($this->once()) + ->method('send'); + } $this->shipmentLoader->expects($this->any()) ->method('setShipmentId') ->with($shipmentId); @@ -309,7 +338,7 @@ public function testExecute($formKeyIsValid, $isPost) ->willReturn($order); $order->expects($this->once()) ->method('setCustomerNoteNotify') - ->with(false); + ->with(!empty($sendEmail)); $this->labelGenerator->expects($this->any()) ->method('create') ->with($shipment, $this->request) @@ -340,7 +369,7 @@ public function testExecute($formKeyIsValid, $isPost) ->with(Session::class) ->willReturn($this->session); $arguments = ['order_id' => $orderId]; - $shipment->expects($this->once()) + $shipment->expects($this->any()) ->method('getOrderId') ->willReturn($orderId); $this->prepareRedirect($arguments); @@ -364,11 +393,22 @@ public function testExecute($formKeyIsValid, $isPost) */ public function executeDataProvider() { + /** + * bool $formKeyIsValid + * bool $isPost + * string $sendEmail + * bool $emailEnabled + * bool $shouldEmailBeSent + */ return [ - [false, false], - [true, false], - [false, true], - [true, true] + [false, false, '', false, false], + [true, false, '', false, false], + [false, true, '', false, false], + [true, true, '', false, false], + [true, true, '', true, false], + [true, true, 'on', false, false], + [true, true, 'on', true, true], + ]; } diff --git a/app/code/Magento/Shipping/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Shipping/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..e676b698c7f0e --- /dev/null +++ b/app/code/Magento/Shipping/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,152 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Shipping\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Shipping\Model\CarrierFactory; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Shipping\Model\Config; + +/** + * Test for \Magento\Shipping\Model\Config. + */ +class ConfigTest extends TestCase +{ + private const STUB_STORE_CODE = 'default'; + + /** + * @var array + */ + private $shippingCarriersData = [ + 'flatrate' => [ + 'active' => '1', + 'name' => 'Fixed', + 'title' => 'Flat Rate', + ], + 'tablerate' => [ + 'active' => '0', + 'name' => 'Table Rate', + 'title' => 'Best Way', + ] + ]; + + /** + * @var Config + */ + private $model; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var CarrierFactory|MockObject + */ + private $carrierFactoryMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->carrierFactoryMock = $this->createMock(CarrierFactory::class); + + $this->model = new Config($this->scopeConfigMock, $this->carrierFactoryMock, []); + } + + /** + * Get active carriers when there is no active on the store + * + * @return void + */ + public function testGetActiveCarriersWhenThereIsNoAvailable(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('carriers', ScopeInterface::SCOPE_STORE, null) + ->willReturn(null); + + $this->assertEquals([], $this->model->getActiveCarriers()); + } + + /** + * Test for getActiveCarriers + * + * @return void + */ + public function testGetActiveCarriers(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('carriers', ScopeInterface::SCOPE_STORE, self::STUB_STORE_CODE) + ->willReturn($this->shippingCarriersData); + + $this->scopeConfigMock->expects($this->exactly(2)) + ->method('isSetFlag') + ->withConsecutive( + ['carriers/flatrate/active', ScopeInterface::SCOPE_STORE, self::STUB_STORE_CODE], + ['carriers/tablerate/active', ScopeInterface::SCOPE_STORE, self::STUB_STORE_CODE], + ) + ->willReturnOnConsecutiveCalls( + true, + false, + ); + + $this->carrierFactoryMock->expects($this->once()) + ->method('create') + ->with('flatrate', self::STUB_STORE_CODE) + ->willReturn(true); + + $this->assertEquals(['flatrate' => true], $this->model->getActiveCarriers(self::STUB_STORE_CODE)); + } + + /** + * Get all carriers when there is no carriers available on the store + * + * @return void + */ + public function testGetAllCarriersWhenThereIsNoAvailable(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('carriers', ScopeInterface::SCOPE_STORE, null) + ->willReturn(null); + + $this->assertEquals([], $this->model->getAllCarriers()); + } + + /** + * Test for getAllCarriers + * + * @return void + */ + public function testGetAllCarriers(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('carriers', ScopeInterface::SCOPE_STORE, self::STUB_STORE_CODE) + ->willReturn($this->shippingCarriersData); + + $this->carrierFactoryMock->expects($this->exactly(2)) + ->method('create') + ->withConsecutive( + ['flatrate', self::STUB_STORE_CODE], + ['tablerate', self::STUB_STORE_CODE], + ) + ->willReturnOnConsecutiveCalls( + true, + false, + ); + + $this->assertEquals(['flatrate' => true], $this->model->getAllCarriers(self::STUB_STORE_CODE)); + } +} diff --git a/app/code/Magento/Store/Model/StoreResolver/GetStoresListByWebsiteIds.php b/app/code/Magento/Store/Model/StoreResolver/GetStoresListByWebsiteIds.php new file mode 100644 index 0000000000000..416537caaf0e0 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreResolver/GetStoresListByWebsiteIds.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreResolver; + +use Magento\Store\Api\StoreWebsiteRelationInterface; + +/** + * Retrieves store ids list array by website ids array + */ +class GetStoresListByWebsiteIds +{ + /** + * @var StoreWebsiteRelationInterface + */ + private $storeWebsiteRelation; + + /** + * @param StoreWebsiteRelationInterface $storeWebsiteRelation + */ + public function __construct(StoreWebsiteRelationInterface $storeWebsiteRelation) + { + $this->storeWebsiteRelation = $storeWebsiteRelation; + } + + /** + * Retrieve list of stores by website ids + * + * @param array $websiteIds + * @return array + */ + public function execute(array $websiteIds): array + { + $storeIdsArray = []; + foreach ($websiteIds as $websiteId) { + $storeIdsArray[] = $this->storeWebsiteRelation->getStoreByWebsiteId($websiteId); + } + + return array_merge([], ...$storeIdsArray); + } +} diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml index ab532538cc3f3..e05496efaa152 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontCustomerCanChangeProductOptionsUsingSwatchesTest.xml @@ -22,8 +22,10 @@ <createData entity="Simple_US_Customer" after="createCategory" stepKey="createCustomer"/> </before> <after> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createCustomer" before="goToProductAttributes" stepKey="deleteCustomer"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" after="deleteCustomer" stepKey="logoutFromCustomer"/> + <comment userInput="BIC workaround" stepKey="logoutFromCustomer"/> </after> <!-- Remove steps that are not used for this test --> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminGoToNewTaxRulePageActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminGoToNewTaxRulePageActionGroup.xml new file mode 100644 index 0000000000000..ce16cfb73df67 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminGoToNewTaxRulePageActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGoToNewTaxRulePageActionGroup"> + <annotations> + <description>Go to the create New Tax Rule page.</description> + </annotations> + <amOnPage url="{{AdminNewTaxRulePage.url}}" stepKey="goToNewTaxRulePage"/> + <waitForPageLoad stepKey="waitForTaxRulePage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminSaveTaxRuleActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminSaveTaxRuleActionGroup.xml new file mode 100644 index 0000000000000..cf9f734b06f2d --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminSaveTaxRuleActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSaveTaxRuleActionGroup"> + <annotations> + <description>Clicks the Save Rule button on the Tax Rule page.</description> + </annotations> + <waitForElementVisible selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="waitForSaveButton"/> + <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForSave"/> + <waitForText selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rule." stepKey="waitForSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml index 84278468a0590..8bacc134c7ab8 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml @@ -154,8 +154,10 @@ </actionGroup> <!--Fill customer group and customer email--> - <selectOption selector="{{AdminOrderFormAccountSection.group}}" userInput="{{GeneralCustomerGroup.code}}" stepKey="selectCustomerGroup"/> - <fillField selector="{{AdminOrderFormAccountSection.email}}" userInput="{{Simple_US_Customer.email}}" stepKey="fillCustomerEmail"/> + <comment userInput="Comment is added to preserve the step key for backward compatibility" stepKey="selectCustomerGroup"/> + <actionGroup ref="AdminFillAccountInformationOnCreateOrderPageActionGroup" stepKey="fillCustomerEmail"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> <!--Fill customer address information--> <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerAddress"> diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml index 0ba20d201f909..0bfdc0eed289c 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml @@ -77,7 +77,7 @@ <waitForPageLoad stepKey="wait6" /> <!--see widget on Storefront--> <see userInput="Hello TinyMCE3!" stepKey="seeContent2"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <actionGroup ref="CliEnableTinyMCE4ActionGroup" stepKey="switchToTinyMCE4" /> <after> <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertCurrentPageNumberActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertCurrentPageNumberActionGroup.xml new file mode 100644 index 0000000000000..1765dbe1e7fcd --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertCurrentPageNumberActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGridAssertCurrentPageNumberActionGroup"> + <annotations> + <description> + Assert current page number on admin grid + </description> + </annotations> + <arguments> + <argument name="expectedCurrentPageNumber" defaultValue="1" type="string"/> + </arguments> + + <seeInField selector="{{AdminDataGridPaginationSection.currentPage}}" userInput="{{expectedCurrentPageNumber}}" stepKey="seeCurrentPageNumberOnGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertTotalPageCountActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertTotalPageCountActionGroup.xml new file mode 100644 index 0000000000000..402bd077f9fbb --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridAssertTotalPageCountActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGridAssertTotalPageCountActionGroup"> + <annotations> + <description> + Assert total page count on admin grid + </description> + </annotations> + <arguments> + <argument name="expectedTotalPageCount" defaultValue="1" type="string"/> + </arguments> + + <waitForElementVisible selector="{{AdminDataGridPaginationSection.totalPagesCount('expectedTotalPageCount')}}" stepKey="waitForTotalPagesCountToBeVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridGoToNextPageActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridGoToNextPageActionGroup.xml new file mode 100644 index 0000000000000..7da082a279688 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminGridGoToNextPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGridGoToNextPageActionGroup"> + <annotations> + <description> + Go to next page of the admin grid. + </description> + </annotations> + + <click selector="{{AdminDataGridPaginationSection.nextPage}}" stepKey="clickNextPageOnGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml index 51cebdb01a74d..eaea88fdddb03 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml @@ -20,6 +20,7 @@ <element name="previousPage" type="button" selector="div.admin__data-grid-pager > button.action-previous" timeout="30"/> <element name="currentPage" type="input" selector="div.admin__data-grid-pager > input[data-ui-id='current-page-input']"/> <element name="totalPages" type="text" selector="div.admin__data-grid-pager > label"/> + <element name="totalPagesCount" type="text" selector="//div[@class='admin__data-grid-pager']//label[@class='admin__control-support-text' and .='of {{arg1}}']" parameterized="true"/> <element name="perPageDropDownValue" type="input" selector=".selectmenu-value input" timeout="30"/> <element name="selectedPage" type="input" selector="#sales_order_create_search_grid_page-current" timeout="30"/> <element name="nextPageActive" type="button" selector="div.admin__data-grid-pager > button.action-next:not(.disabled)" timeout="30"/> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml index c5b000259e265..d2d39076bcfbb 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridTableSection.xml @@ -22,5 +22,6 @@ <element name="rowTemplateStrict" type="block" selector="//tbody/tr[td[*[text()[normalize-space()='{{text}}']]]]" parameterized="true" /> <element name="rowTemplate" type="block" selector="//tbody/tr[td[*[contains(.,normalize-space('{{text}}'))]]]" parameterized="true" timeout="30" /> <element name="firstNotEmptyRow" type="block" selector="table.data-grid tbody tr[data-role=row]:not(.data-grid-tr-no-data):nth-of-type(1)" timeout="30"/> + <element name="firstNotEmptyRow2" type="block" selector="table.data-grid tbody tr:not(.data-grid-tr-no-data):nth-of-type(1)" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/date.js b/app/code/Magento/Ui/view/base/web/js/form/element/date.js index 1432372dd75a9..af9142745206b 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/date.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/date.js @@ -127,7 +127,7 @@ define([ if (this.options.showsTime) { shiftedValue = moment.tz(value, 'UTC').tz(this.storeTimeZone); } else { - shiftedValue = moment(value, this.outputDateFormat); + shiftedValue = moment(value, this.outputDateFormat, true); } if (!shiftedValue.isValid()) { diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/website.js b/app/code/Magento/Ui/view/base/web/js/form/element/website.js index 3c99cc0874cf9..0d1ed2d9961a1 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/website.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/website.js @@ -26,10 +26,6 @@ define([ initialize: function () { this._super(); - if (this.customerId || this.isGlobalScope) { - this.disable(true); - } - return this; } }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js b/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js index 534d292809ed1..5f6c21cf6167f 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/paging/paging.js @@ -36,7 +36,8 @@ define([ imports: { totalSelected: '${ $.selectProvider }:totalSelected', totalRecords: '${ $.provider }:data.totalRecords', - filters: '${ $.provider }:params.filters' + filters: '${ $.provider }:params.filters', + keywordUpdated: '${ $.provider }:params.keywordUpdated' }, exports: { @@ -58,7 +59,8 @@ define([ 'pages': 'onPagesChange', 'pageSize': 'onPageSizeChange', 'totalRecords': 'updateCounter', - '${ $.provider }:params.filters': 'goFirst' + '${ $.provider }:params.filters': 'goFirst', + '${ $.provider }:params.search': 'onSearchUpdate' }, modules: { @@ -282,6 +284,17 @@ define([ */ onPagesChange: function () { this.updateCursor(); + }, + + /** + * Resent the pagination to Page 1 on search keyword update + */ + onSearchUpdate: function () { + if (!_.isUndefined(this.keywordUpdated) && this.keywordUpdated) { + this.goFirst(); + } + + return this; } }); }); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js index 3f5434761ba18..d4db0100db7c6 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js @@ -22,6 +22,7 @@ define([ placeholder: $t('Search by keyword'), label: $t('Keyword'), value: '', + keywordUpdated: false, previews: [], chipsProvider: 'componentType = filtersChips, ns = ${ $.ns }', statefull: { @@ -31,7 +32,8 @@ define([ value: true, previews: true, inputValue: true, - focused: true + focused: true, + keywordUpdated: true }, imports: { inputValue: 'value', @@ -39,7 +41,8 @@ define([ focused: false }, exports: { - value: '${ $.provider }:params.search' + value: '${ $.provider }:params.search', + keywordUpdated: '${ $.provider }:params.keywordUpdated' }, modules: { chips: '${ $.chipsProvider }' @@ -124,6 +127,7 @@ define([ apply: function (value) { value = value || this.inputValue; + this.keywordUpdated = this.value !== value; this.value = this.inputValue = value.trim(); return this; diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup.xml new file mode 100644 index 0000000000000..2e80fa0f798a6 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup" extends="AdminSearchByRequestPathActionGroup"> + <annotations> + <description>Goes to the Admin URL Rewrite grid page. Searches the grid based on the provided Redirect Path and StoreView name. Validates that the provided Redirect Path, Type and Target Path are present and correct in the grid.</description> + </annotations> + <arguments> + <argument name="storeView" type="string"/> + </arguments> + <selectOption selector="{{AdminDataGridHeaderSection.filterFieldSelect('store_id')}}" userInput="{{storeView}}" stepKey="fillStoreView" after="fillRedirectPathFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml index c14d0b175d2c0..ccd4297312f44 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml @@ -109,10 +109,11 @@ </actionGroup> <grabFromCurrentUrl stepKey="productId" regex="#\/([0-9]*)?\/$#"/> <!-- Open Url Rewrite page and verify new Redirect Path, RedirectType and Target Path for the grabbed product Id --> - <actionGroup ref="AdminSearchByRequestPathActionGroup" stepKey="searchPath1"> + <actionGroup ref="AdminFilterUrlRewriteGridByRequestPathAndStoreViewActionGroup" stepKey="searchPath1"> <argument name="redirectPath" value="$$createProduct.name$$.html"/> <argument name="redirectType" value="No"/> <argument name="targetPath" value="catalog/product/view/id/{$productId}"/> + <argument name="storeView" value="{{storeViewData.name}}"/> </actionGroup> <actionGroup ref="AssertAdminStoreValueIsSetForUrlRewriteActionGroup" stepKey="seeStoreValueForProductId"> diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index 6430f71765fe4..0b372555fe4c2 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -9,6 +9,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Uid; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -36,16 +37,24 @@ class EntityUrl implements ResolverInterface */ private $redirectType; + /** + * @var Uid + */ + private $idEncoder; + /** * @param UrlFinderInterface $urlFinder * @param CustomUrlLocatorInterface $customUrlLocator + * @param Uid $idEncoder */ public function __construct( UrlFinderInterface $urlFinder, - CustomUrlLocatorInterface $customUrlLocator + CustomUrlLocatorInterface $customUrlLocator, + Uid $idEncoder ) { $this->urlFinder = $urlFinder; $this->customUrlLocator = $customUrlLocator; + $this->idEncoder = $idEncoder; } /** @@ -78,6 +87,7 @@ public function resolve( $relativeUrl = $finalUrlRewrite->getRequestPath(); $resultArray = $this->rewriteCustomUrls($finalUrlRewrite, $storeId) ?? [ 'id' => $finalUrlRewrite->getEntityId(), + 'entity_uid' => $this->idEncoder->encode((string)$finalUrlRewrite->getEntityId()), 'canonical_url' => $relativeUrl, 'relative_url' => $relativeUrl, 'redirectCode' => $this->redirectType, @@ -115,6 +125,7 @@ private function rewriteCustomUrls(UrlRewrite $finalUrlRewrite, int $storeId): ? ? $finalCustomUrlRewrite->getRequestPath() : $finalUrlRewrite->getRequestPath(); return [ 'id' => $finalUrlRewrite->getEntityId(), + 'entity_uid' => $this->idEncoder->encode((string)$finalUrlRewrite->getEntityId()), 'canonical_url' => $relativeUrl, 'relative_url' => $relativeUrl, 'redirectCode' => $finalCustomUrlRewrite->getRedirectType(), diff --git a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls index 7f7ebb627b4dc..7000f52d7d683 100644 --- a/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/UrlRewriteGraphQl/etc/schema.graphqls @@ -6,7 +6,8 @@ type Query { } type EntityUrl @doc(description: "EntityUrl is an output object containing the `id`, `relative_url`, and `type` attributes") { - id: Int @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") + id: Int @deprecated(reason: "Use `entity_uid` instead.") @doc(description: "The ID assigned to the object associated with the specified url. This could be a product ID, category ID, or page ID.") + entity_uid: ID @doc(description: "The unique ID for a `ProductInterface`, `CategoryInterface`, `CmsPage`, etc. object associated with the specified url. This could be a product UID, category UID, or cms page UID.") canonical_url: String @deprecated(reason: "The canonical_url field is deprecated, use relative_url instead.") relative_url: String @doc(description: "The internal relative URL. If the specified url is a redirect, the query returns the redirected URL, not the original.") redirectCode: Int @doc(description: "301 or 302 HTTP code for url permanent or temporary redirect or 0 for the 200 no redirect") diff --git a/app/code/Magento/User/etc/adminhtml/system.xml b/app/code/Magento/User/etc/adminhtml/system.xml index 584b40a023c93..8b619c4f9cf48 100644 --- a/app/code/Magento/User/etc/adminhtml/system.xml +++ b/app/code/Magento/User/etc/adminhtml/system.xml @@ -14,6 +14,11 @@ <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> </field> + <field id="new_user_notification_template" translate="label comment" type="select" sortOrder="50" showInDefault="1" canRestore="1"> + <label>New User Notification Template</label> + <comment>Email template chosen based on theme fallback when "Default" option is selected.</comment> + <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model> + </field> </group> <group id="security"> <field id="lockout_failures" translate="label comment" sortOrder="100" showInDefault="1" canRestore="1"> diff --git a/app/code/Magento/User/view/adminhtml/email/new_user_notification.html b/app/code/Magento/User/view/adminhtml/email/new_user_notification.html index 87f4e4669c4b6..0b6ceeb61cb71 100644 --- a/app/code/Magento/User/view/adminhtml/email/new_user_notification.html +++ b/app/code/Magento/User/view/adminhtml/email/new_user_notification.html @@ -4,12 +4,12 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "New admin user '%user_name' created" user_name=$user.name}} @--> +<!--@subject {{trans "New admin user '%user_name' created" user_name=$user.username}} @--> <!--@vars { "var store.frontend_name":"Store Name", -"var user.name":"User Name", -"var user.first_name":"User First Name", -"var user.last_name":"User Last Name", +"var user.username":"User Name", +"var user.firstname":"User First Name", +"var user.lastname":"User Last Name", "var user.email":"User Email", "var store_email":"Store Email", "var store_phone":"Store Phone" @@ -17,7 +17,7 @@ {{trans "Hello,"}} -{{trans "A new admin account was created for %first_name, %last_name using %email." first_name=$user.first_name last_name=$user.last_name email=$user.email}} +{{trans "A new admin account was created for %first_name, %last_name using %email." first_name=$user.firstname last_name=$user.lastname email=$user.email}} {{trans "If you have not authorized this action, please contact us immediately at %store_email" store_email=$store_email |escape}}{{depend store_phone}} {{trans "or call us at %store_phone" store_phone=$store_phone |escape}}{{/depend}}. {{trans "Thanks,"}} diff --git a/app/code/Magento/Weee/Test/Mftf/Data/FixedProductAttributeData.xml b/app/code/Magento/Weee/Test/Mftf/Data/FixedProductAttributeData.xml index b8b45d84242c9..071f96bb65266 100644 --- a/app/code/Magento/Weee/Test/Mftf/Data/FixedProductAttributeData.xml +++ b/app/code/Magento/Weee/Test/Mftf/Data/FixedProductAttributeData.xml @@ -8,7 +8,7 @@ <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="productFPTAttribute" type="ProductAttribute"> + <entity name="productFPTAttribute" type="ProductAttribute" deprecated="Use FPTProductAttribute instead"> <data key="attribute_code" unique="suffix">attribute</data> <data key="is_unique">true</data> <data key="frontend_input">weee</data> @@ -17,4 +17,13 @@ <data key="is_filterable_in_grid">true</data> <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabel</requiredEntity> </entity> + <entity name="FPTProductAttribute" type="ProductAttribute"> + <data key="attribute_code" unique="suffix">weee_attribute</data> + <data key="is_unique">true</data> + <data key="frontend_input">weee</data> + <data key="is_used_in_grid">true</data> + <data key="is_visible_in_grid">true</data> + <data key="is_filterable_in_grid">true</data> + <requiredEntity type="FrontendLabel">ProductAttributeFrontendLabelWeee</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/Weee/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Weee/Test/Mftf/Data/FrontendLabelData.xml new file mode 100644 index 0000000000000..7c362ba0ee303 --- /dev/null +++ b/app/code/Magento/Weee/Test/Mftf/Data/FrontendLabelData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="ProductAttributeFrontendLabelWeee" type="FrontendLabel"> + <data key="store_id">0</data> + <data key="label" unique="suffix">weee-attribute</data> + </entity> +</entities> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml index 0f4a7f9a55d26..ccbd431848dbc 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml @@ -22,7 +22,7 @@ <before> <!-- Create product attribute and add it to default attribute set />--> <comment userInput="Create product attribute and add it to default attribute set" stepKey="createAttrAndAddToDefaultAttrSet"/> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addToDefaultAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml index 0d7c21b6efffc..4e70c9ba87d64 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml @@ -18,7 +18,7 @@ <group value="weee"/> </annotations> <before> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml index e78036458301b..833f619888bfb 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml @@ -27,7 +27,7 @@ <!-- Tax Rule is created based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> <!-- Fixed Product Tax attribute is created and added to default attribute set --> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml index dda125835110a..8e8667cb7e13d 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerVirtualQuoteTest.xml @@ -27,7 +27,7 @@ <!-- Tax Rule is created based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> <!-- Fixed Product Tax attribute is created and added to default attribute set --> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml index 74ba7c1f2bff3..3a3f9c7e8931a 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml @@ -27,7 +27,7 @@ <!-- Tax Rule is created based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> <!-- Fixed Product Tax attribute is created and added to default attribute set --> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml index 495b9a990a465..0d54991f84395 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestVirtualQuoteTest.xml @@ -27,7 +27,7 @@ <!-- Tax Rule is created based on default tax rates (Stores>Tax Rule) US-CA-*-Rate 1 = 8.2500 US-NY-*-Rate 1 = 8.3750 --> <createData entity="SimpleTaxRule" stepKey="createTaxRule"/> <!-- Fixed Product Tax attribute is created and added to default attribute set --> - <createData entity="productFPTAttribute" stepKey="createProductFPTAttribute"/> + <createData entity="FPTProductAttribute" stepKey="createProductFPTAttribute"/> <createData entity="AddToDefaultSet" stepKey="addFPTToAttributeSet"> <requiredEntity createDataKey="createProductFPTAttribute"/> </createData> diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php index c48bf9e7e4c7a..a704a5676f632 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/Layout.php @@ -315,25 +315,6 @@ public function getAddLayoutButtonHtml() return $button->toHtml(); } - /** - * Retrieve remove layout button html - * - * @return string - */ - public function getRemoveLayoutButtonHtml() - { - $button = $this->getLayout()->createBlock( - \Magento\Backend\Block\Widget\Button::class - )->setData( - [ - 'label' => $this->escapeHtmlAttr(__('Remove Layout Update')), - 'onclick' => 'WidgetInstance.removePageGroup(this)', - 'class' => 'action-delete', - ] - ); - return $button->toHtml(); - } - /** * Prepare and retrieve page groups data of widget instance * diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml index e657b3eb73b53..5cf135f158130 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetActionGroup.xml @@ -23,6 +23,10 @@ <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widget.name}}" stepKey="fillTitle"/> <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" userInput="{{widget.store_ids[0]}}" stepKey="setWidgetStoreIds"/> <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate2"/> + <seeNumberOfElements userInput="2" selector="{{AdminNewWidgetSection.layoutUpdate}}" stepKey="seeTwoLayoutUpdates"/> + <click selector="{{AdminNewWidgetSection.removeLastLayoutUpdate}}" stepKey="clickRemoveLastLayoutUpdate"/> + <seeNumberOfElements userInput="1" selector="{{AdminNewWidgetSection.layoutUpdate}}" stepKey="seeOneLayoutUpdate"/> <selectOption selector="{{AdminNewWidgetSection.selectDisplayOn}}" userInput="{{widget.display_on}}" stepKey="setDisplayOn"/> <waitForAjaxLoad stepKey="waitForLoad"/> <selectOption selector="{{AdminNewWidgetSection.selectContainer}}" userInput="{{widget.container}}" stepKey="setContainer"/> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetWthoutLayoutActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetWthoutLayoutActionGroup.xml new file mode 100644 index 0000000000000..e9ee80c1a5f2a --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminCreateWidgetWthoutLayoutActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateWidgetWthoutLayoutActionGroup"> + <annotations> + <description>Goes to the Admin Widget creation page without saving it</description> + </annotations> + <arguments> + <argument name="widget"/> + </arguments> + <amOnPage url="{{AdminNewWidgetPage.url}}" stepKey="amOnAdminNewWidgetPage"/> + <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widget.type}}" stepKey="setWidgetType"/> + <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widget.design_theme}}" stepKey="setWidgetDesignTheme"/> + <click selector="{{AdminNewWidgetSection.continue}}" stepKey="clickContinue"/> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widget.name}}" stepKey="fillTitle"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" userInput="{{widget.store_ids[0]}}" stepKey="setWidgetStoreIds"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetAddLayoutUpdateActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetAddLayoutUpdateActionGroup.xml new file mode 100644 index 0000000000000..fa73fa4926e10 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetAddLayoutUpdateActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminWidgetAddLayoutUpdateActionGroup"> + <annotations> + <description>Add layouts during widgets creation</description> + </annotations> + <waitForAjaxLoad stepKey="waitForLoad"/> + <click selector="{{AdminNewWidgetSection.addLayoutUpdate}}" stepKey="clickAddLayoutUpdate"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetDeleteLayoutUpdateActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetDeleteLayoutUpdateActionGroup.xml new file mode 100644 index 0000000000000..e52fb1a7f6514 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminWidgetDeleteLayoutUpdateActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminWidgetDeleteLayoutUpdateActionGroup"> + <annotations> + <description>Delete layouts during widgets creation</description> + </annotations> + <click selector="{{AdminNewWidgetSection.deleteWidgetLayoutAction}}" stepKey="clickFirstDeleteButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index 8a17b589d7ab2..1ed77904d3a68 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -17,6 +17,8 @@ <element name="widgetStoreIds" type="select" selector="#store_ids"/> <element name="widgetSortOrder" type="input" selector="#sort_order"/> <element name="addLayoutUpdate" type="button" selector=".action-default.scalable.action-add"/> + <element name="layoutUpdate" type="block" selector=".page_group_container"/> + <element name="removeLastLayoutUpdate" type="button" selector=".page_group_container:last-child .action-default.scalable.action-delete"/> <element name="selectDisplayOn" type="select" selector="#widget_instance[0][page_group]"/> <element name="selectContainer" type="select" selector="#all_pages_0>table>tbody>tr>td:nth-child(1)>div>div>select"/> <element name="displayOnByIndex" type="select" selector="select[name='widget_instance[{{index}}][page_group]']" parameterized="true"/> @@ -52,6 +54,8 @@ <element name="displayPageControl" type="select" selector="[name='parameters[show_pager]']"/> <element name="numberOfProductsToDisplay" type="input" selector="[name='parameters[products_count]']"/> <element name="cacheLifetime" type="input" selector="[name='parameters[cache_lifetime]']"/> + <element name="deleteWidgetLayoutAction" type="button" selector="#page_group_container > div:first-of-type > div.fieldset-wrapper-title > div > .action-default.action-delete"/> + <element name="CountDeleteButtons" type="button" selector="#page_group_container > .fieldset-wrapper.page_group_container > div.fieldset-wrapper-title > div > .action-default.action-delete"/> </section> </sections> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml new file mode 100644 index 0000000000000..04c1552d53522 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminWidgetAddAndDeleteMultipleLayoutSectionsTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminWidgetAddAndDeleteMultipleLayoutSectionsTest"> + <annotations> + <features value="Widget"/> + <stories value="Add and Delete multiple layouts when creating a Widget"/> + <title value="Add and Delete multiple layouts"/> + <description value="Admin should be able to Add and Delete multiple layouts"/> + <severity value="CRITICAL"/> + <group value="Widget"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToContentWidgetsPageFirst"> + <argument name="menuUiId" value="{{AdminMenuContent.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuContentElementsWidgets.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminAssertPageTitleActionGroup" stepKey="seePageTitleFirst"> + <argument name="title" value="{{AdminMenuContentElementsWidgets.pageTitle}}"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear1"/> + <actionGroup ref="AdminCreateWidgetWthoutLayoutActionGroup" stepKey="addWidgetForTest"> + <argument name="widget" value="ProductsListWidget"/> + </actionGroup> + <actionGroup ref="AdminWidgetAddLayoutUpdateActionGroup" stepKey="AddSecondLayout"/> + <actionGroup ref="AdminWidgetAddLayoutUpdateActionGroup" stepKey="AddThirdLayout"/> + <seeNumberOfElements userInput="3" selector="{{AdminNewWidgetSection.CountDeleteButtons}}" stepKey="seeThreeDeleteButtons"/> + <actionGroup ref="AdminWidgetDeleteLayoutUpdateActionGroup" stepKey="DeleteFirstLayoutForWidget"></actionGroup> + <seeNumberOfElements userInput="2" selector="{{AdminNewWidgetSection.CountDeleteButtons}}" stepKey="seeTwoDeleteButtons"/> + <actionGroup ref="AdminWidgetDeleteLayoutUpdateActionGroup" stepKey="DeleteSecondLayoutForWidget"></actionGroup> + <seeNumberOfElements userInput="1" selector="{{AdminNewWidgetSection.CountDeleteButtons}}" stepKey="seeOneDeleteButtons"/> + <actionGroup ref="AdminWidgetDeleteLayoutUpdateActionGroup" stepKey="DeleteThirdLayoutForWidget"></actionGroup> + <seeNumberOfElements userInput="0" selector="{{AdminNewWidgetSection.CountDeleteButtons}}" stepKey="seeZeroDeleteButtons"/> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml index 93c5cac33f947..2d4f88709fd91 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml @@ -38,7 +38,8 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" '<label for="widget_instance[<%- data.id %>][page_group]">Display on <span class="required">*</span></label>'+ '{$block->getDisplayOnSelectHtml()}'+ '<div class="actions">'+ - {$jsonHelper->jsonEncode($block->getRemoveLayoutButtonHtml())} + + '<button title="{$escaper->escapeHtmlAttr(__('Remove Layout Update'))}" type="button"'+ + ' class="action-default scalable action-delete" onclick="WidgetInstance.removePageGroup(this)" />'+ '</div>'+ '</div>'+ '<div class="fieldset-wrapper-content">'+ diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index aafd8b0b0d4d3..8fe3d3c707900 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -36,6 +36,8 @@ </before> <after> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> <deleteData createDataKey="secondProduct" stepKey="deleteSecondProduct"/> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> @@ -49,7 +51,7 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsFilters"/> <!--Logout everywhere--> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <comment userInput="BIC workaround" stepKey="customerLogout"/> </after> <!-- Change products visibility on store-view level --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml index 689b76e42e6f1..96e5417f1cffd 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/WishListWithDisabledProductTest.xml @@ -23,9 +23,11 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> + <!-- Logout customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutStorefront"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <comment userInput="BIC workaround" stepKey="customerLogout"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> diff --git a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php index 9cc1404613e41..96e39b6566d5d 100644 --- a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php +++ b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php @@ -7,6 +7,7 @@ namespace Magento\WishlistGraphQl\Mapper; +use Magento\Framework\GraphQl\Schema\Type\Enum\DataMapperInterface; use Magento\Wishlist\Model\Wishlist; /** @@ -14,6 +15,20 @@ */ class WishlistDataMapper { + /** + * @var DataMapperInterface + */ + private $enumDataMapper; + + /** + * @param DataMapperInterface $enumDataMapper + */ + public function __construct( + DataMapperInterface $enumDataMapper + ) { + $this->enumDataMapper = $enumDataMapper; + } + /** * Mapping the review data * @@ -29,7 +44,26 @@ public function map(Wishlist $wishlist): array 'updated_at' => $wishlist->getUpdatedAt(), 'items_count' => $wishlist->getItemsCount(), 'name' => $wishlist->getName(), + 'visibility' => $this->getMappedVisibility((int) $wishlist->getVisibility()), 'model' => $wishlist, ]; } + + /** + * Get wishlist mapped visibility + * + * @param int $visibility + * + * @return string|null + */ + private function getMappedVisibility(int $visibility): ?string + { + if ($visibility === null) { + return null; + } + + $visibilityEnums = $this->enumDataMapper->getMappedEnums('WishlistVisibilityEnum'); + + return isset($visibilityEnums[$visibility]) ? strtoupper($visibilityEnums[$visibility]) : null; + } } diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php index 77ff483a60bd2..bf9fe1875c228 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php @@ -61,7 +61,9 @@ public function resolve( /** @var Wishlist $wishlist */ $wishlist = $value['model']; - $wishlistItems = $this->getWishListItems($wishlist); + /** @var WishlistItemCollection $wishlistItemCollection */ + $wishlistItemsCollection = $this->getWishListItems($wishlist, $args); + $wishlistItems = $wishlistItemsCollection->getItems(); $data = []; foreach ($wishlistItems as $wishlistItem) { @@ -74,17 +76,28 @@ public function resolve( 'itemModel' => $wishlistItem, ]; } - return $data; + return [ + 'items' => $data, + 'page_info' => [ + 'current_page' => $wishlistItemsCollection->getCurPage(), + 'page_size' => $wishlistItemsCollection->getPageSize(), + 'total_pages' => $wishlistItemsCollection->getLastPageNumber() + ] + ]; } /** * Get wishlist items * * @param Wishlist $wishlist - * @return Item[] + * @param array $args + * @return WishlistItemCollection */ - private function getWishListItems(Wishlist $wishlist): array + private function getWishListItems(Wishlist $wishlist, array $args): WishlistItemCollection { + $currentPage = $args['currentPage'] ?? 1; + $pageSize = $args['pageSize'] ?? 20; + /** @var WishlistItemCollection $wishlistItemCollection */ $wishlistItemCollection = $this->wishlistItemCollectionFactory->create(); $wishlistItemCollection @@ -93,6 +106,13 @@ private function getWishListItems(Wishlist $wishlist): array return $store->getId(); }, $this->storeManager->getStores())) ->setVisibilityFilter(); - return $wishlistItemCollection->getItems(); + if ($currentPage > 0) { + $wishlistItemCollection->setCurPage($currentPage); + } + + if ($pageSize > 0) { + $wishlistItemCollection->setPageSize($pageSize); + } + return $wishlistItemCollection; } } diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index 7812176db60d0..5a44facb606ac 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -11,7 +11,7 @@ type Customer { currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1.") ): [Wishlist!]! @doc(description: "An array of wishlists. In Magento Open Source, customers are limited to one wish list. The number of wish lists is configurable for Magento Commerce") @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlists") wishlist: Wishlist! @deprecated(reason: "Use `Customer.wishlists` or `Customer.wishlist_v2`") @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "Contains a customer's wish lists") @cache(cacheable: false) - wishlist_v2(id: ID!): Wishlist @doc(description: "Retrieve the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistById") + wishlist_v2(id: ID!): Wishlist @doc(description: "Retrieve the specified wish list identified by the unique ID for a `Wishlist` object") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistById") } type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be used instead") { @@ -23,16 +23,19 @@ type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be use } type Wishlist { - id: ID @doc(description: "Wishlist unique identifier") + id: ID @doc(description: "The unique ID for a `Wishlist` object") items: [WishlistItem] @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItemsResolver") @deprecated(reason: "Use field `items_v2` from type `Wishlist` instead") - items_v2: [WishlistItemInterface] @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItems") @doc(description: "An array of items in the customer's wish list") + items_v2( + currentPage: Int = 1, + pageSize: Int = 20 + ): WishlistItems @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItems") @doc(description: "An array of items in the customer's wish list") items_count: Int @doc(description: "The number of items in the wish list") sharing_code: String @doc(description: "An encrypted code that Magento uses to link to the wish list") updated_at: String @doc(description: "The time of the last modification to the wish list") } interface WishlistItemInterface @typeResolver(class: "Magento\\WishlistGraphQl\\Model\\Resolver\\Type\\WishlistItemType") { - id: ID! @doc(description: "The ID of the wish list item") + id: ID! @doc(description: "The unique ID for a `WishlistItemInterface` object") quantity: Float! @doc(description: "The quantity of this wish list item") description: String @doc(description: "The description of the item") added_at: String! @doc(description: "The date and time the item was added to the wish list") @@ -40,8 +43,13 @@ interface WishlistItemInterface @typeResolver(class: "Magento\\WishlistGraphQl\\ customizable_options: [SelectedCustomizableOption] @doc(description: "Custom options selected for the wish list item") } +type WishlistItems { + items: [WishlistItemInterface]! @doc(description: "A list of items in the wish list") + page_info: SearchResultPageInfo @doc(description: "Contains pagination metadata") +} + type WishlistItem { - id: Int @doc(description: "The wish list item ID") + id: Int @doc(description: "The unique ID for a `WishlistItem` object") qty: Float @doc(description: "The quantity of this wish list item"), description: String @doc(description: "The customer's comment about this item"), added_at: String @doc(description: "The time when the customer added the item to the wish list"), @@ -73,7 +81,7 @@ type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's } input WishlistItemUpdateInput @doc(description: "Defines updates to items in a wish list") { - wishlist_item_id: ID! @doc(description: "The ID of the wishlist item to update") + wishlist_item_id: ID! @doc(description: "The unique ID for a `WishlistItemInterface` object") quantity: Float @doc(description: "The new amount or number of this item") description: String @doc(description: "Customer-entered comments about the item") selected_options: [ID!] @doc(description: "An array of strings corresponding to options the customer selected") diff --git a/app/etc/di.xml b/app/etc/di.xml index d4651499e6fce..b1d81ed70f6b4 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -229,6 +229,17 @@ </arguments> </type> <preference for="Magento\AsynchronousOperations\Model\ConfigInterface" type="Magento\WebapiAsync\Model\Config\Proxy" /> + <virtualType name="Magento\Framework\Communication\Config\Reader\XmlReader" type="Magento\Framework\Config\Reader\Filesystem"> + <arguments> + <argument name="converter" xsi:type="object">Magento\Framework\Communication\Config\Reader\XmlReader\Converter</argument> + <argument name="schemaLocator" xsi:type="object">Magento\Framework\Communication\Config\Reader\XmlReader\SchemaLocator</argument> + <argument name="fileName" xsi:type="string">communication.xml</argument> + <argument name="idAttributes" xsi:type="array"> + <item name="/config/topic" xsi:type="string">name</item> + <item name="/config/topic/handler" xsi:type="string">name</item> + </argument> + </arguments> + </virtualType> <type name="Magento\Framework\Communication\Config\CompositeReader"> <arguments> <argument name="readers" xsi:type="array"> diff --git a/composer.json b/composer.json index b5a484d3828b8..6aa9355cec7b1 100644 --- a/composer.json +++ b/composer.json @@ -139,6 +139,7 @@ "magento/module-checkout-agreements-graph-ql": "*", "magento/module-cms": "*", "magento/module-cms-url-rewrite": "*", + "magento/module-compare-list-graph-ql": "*", "magento/module-config": "*", "magento/module-configurable-import-export": "*", "magento/module-configurable-product": "*", @@ -199,6 +200,7 @@ "magento/module-login-as-customer-api": "*", "magento/module-login-as-customer-assistance": "*", "magento/module-login-as-customer-frontend-ui": "*", + "magento/module-login-as-customer-graph-ql": "*", "magento/module-login-as-customer-log": "*", "magento/module-login-as-customer-quote": "*", "magento/module-login-as-customer-page-cache": "*", @@ -327,7 +329,7 @@ "tinymce/tinymce": "3.4.7", "magento/module-tinymce-3": "*", "magento/module-csp": "*", - "magento/module-aws-s-3": "*", + "magento/module-aws-s3": "*", "magento/module-remote-storage": "*" }, "conflict": { diff --git a/composer.lock b/composer.lock index f4ece70a22e62..b0b15f4e13f6c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "50fd3418a729ef9b577d214fe6c9b0b1", + "content-hash": "ac6fc13ba98a815bce589d300d28012c", "packages": [ { "name": "aws/aws-sdk-php", @@ -1459,12 +1459,6 @@ "BSD-3-Clause" ], "description": "Replace zendframework and zfcampus packages with their Laminas Project equivalents.", - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-05-20T13:45:39+00:00" }, { @@ -3720,13 +3714,11 @@ "Magento\\Composer\\": "src" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "OSL-3.0", "AFL-3.0" ], - "description": "Magento composer library helps to instantiate Composer application and run composer commands.", - "time": "2020-06-15T17:52:31+00:00" + "description": "Magento composer library helps to instantiate Composer application and run composer commands." }, { "name": "magento/magento-composer-installer", @@ -7556,20 +7548,6 @@ "parser", "php" ], - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], "time": "2020-05-25T17:44:05+00:00" }, { @@ -8461,21 +8439,21 @@ }, { "name": "magento/magento-coding-standard", - "version": "5", + "version": "6", "source": { "type": "git", "url": "https://github.com/magento/magento-coding-standard.git", - "reference": "da46c5d57a43c950dfa364edc7f1f0436d5353a5" + "reference": "efc9084db3d1bd145b92d6b8a2e9cb0faec54fa7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/da46c5d57a43c950dfa364edc7f1f0436d5353a5", - "reference": "da46c5d57a43c950dfa364edc7f1f0436d5353a5", + "url": "https://api.github.com/repos/magento/magento-coding-standard/zipball/efc9084db3d1bd145b92d6b8a2e9cb0faec54fa7", + "reference": "efc9084db3d1bd145b92d6b8a2e9cb0faec54fa7", "shasum": "" }, "require": { "php": ">=5.6.0", - "squizlabs/php_codesniffer": "^3.4", + "squizlabs/php_codesniffer": "^3.5", "webonyx/graphql-php": ">=0.12.6 <1.0" }, "require-dev": { @@ -8496,7 +8474,7 @@ "AFL-3.0" ], "description": "A set of Magento specific PHP CodeSniffer rules.", - "time": "2019-11-04T22:08:27+00:00" + "time": "2020-12-03T14:41:54+00:00" }, { "name": "magento/magento2-functional-testing-framework", @@ -9560,20 +9538,6 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" - } - ], "time": "2020-05-05T12:55:44+00:00" }, { @@ -9863,12 +9827,6 @@ "keywords": [ "timer" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-20T06:00:37+00:00" }, { @@ -10013,16 +9971,6 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-05-22T13:54:05+00:00" }, { @@ -12059,5 +12007,6 @@ "ext-zip": "*", "lib-libxml": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "1.1.0" } 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 1a5796e07b08b..85684ead2532e 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls @@ -2,20 +2,22 @@ # See COPYING.txt for license details. type Query { - testItem(id: Int!) : Item @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") + testItem(id: Int!) : TestItemOutput @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") testUnion: TestUnion @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\TestUnion") + testQueryWithNestedMandatoryInputArguments(input: TestInputQueryWithMandatoryArgumentsInput): TestItemOutput + testQueryWithTopLevelMandatoryInputArguments(topLevelArgument: String!): TestItemOutput } type Mutation { - testItem(id: Int!) : MutationItem @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") + testItem(id: Int!) : MutationItemOutput @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") } -type Item { +type TestItemOutput { item_id: Int name: String } -type MutationItem { +type MutationItemOutput { item_id: Int name: String } @@ -30,3 +32,13 @@ type TypeCustom1 { type TypeCustom2 { custom_name2: String } + +input TestInputQueryWithMandatoryArgumentsInput { + query_id: String! + query_items: [QueryWithMandatoryArgumentsInput!]! +} + +input QueryWithMandatoryArgumentsInput { + query_item_id: Int! + quantity: Float +} 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 b970ad8376349..acf1f20e3c006 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQueryExtension/etc/schema.graphqls @@ -1,10 +1,10 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. -type Item { +type TestItemOutput { integer_list: [Int] @resolver(class: "Magento\\TestModuleGraphQlQueryExtension\\Model\\Resolver\\IntegerList") } -type MutationItem { +type MutationItemOutput { integer_list: [Int] @resolver(class: "Magento\\TestModuleGraphQlQueryExtension\\Model\\Resolver\\IntegerList") } diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php index 3ef6e6618c6c5..9e34027db6aea 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiConfigFixture.php @@ -35,7 +35,8 @@ class ApiConfigFixture extends ConfigFixture protected function setStoreConfigValue(array $matches, $configPathAndValue): void { $storeCode = $matches[0]; - [$configScope, $configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 3); + $parts = preg_split('/\s+/', $configPathAndValue, 3); + [$configScope, $configPath, $requiredValue] = $parts + ['', '', '']; /** @var ConfigStorage $configStorage */ $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); if (!$configStorage->checkIsRecordExist($configPath, ScopeInterface::SCOPE_STORES, $storeCode)) { @@ -69,7 +70,8 @@ protected function setGlobalConfigValue($configPathAndValue): void protected function setWebsiteConfigValue(array $matches, $configPathAndValue): void { $websiteCode = $matches[0]; - [$configScope, $configPath, $requiredValue] = preg_split('/\s+/', $configPathAndValue, 3); + $parts = preg_split('/\s+/', $configPathAndValue, 3); + [$configScope, $configPath, $requiredValue] = $parts + ['', '', '']; /** @var ConfigStorage $configStorage */ $configStorage = Bootstrap::getObjectManager()->get(ConfigStorage::class); if (!$configStorage->checkIsRecordExist($configPath, ScopeInterface::SCOPE_WEBSITES, $websiteCode)) { diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php index ba12a02cb5b1f..09987457e3c56 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeMediaGalleryManagementInterfaceTest.php @@ -235,8 +235,8 @@ public function testCreateWithNotDefaultStoreId() $this->assertEquals($updatedImage['file'], $targetProduct->getData('image')); // No values for default store view were provided $this->assertNull($updatedImage['label_default']); - $this->assertNull($updatedImage['position_default']); - $this->assertNull($updatedImage['disabled_default']); + $this->assertEquals(1, $updatedImage['position_default']); + $this->assertEquals(0, $updatedImage['disabled_default']); } /** @@ -483,7 +483,9 @@ public function testCreateThrowsExceptionIfProvidedImageHasWrongMimeType() public function testCreateThrowsExceptionIfTargetProductDoesNotExist() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The product that was requested doesn\'t exist. Verify the product and try again.'); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); $this->createServiceInfo['rest']['resourcePath'] = '/V1/products/wrong_product_sku/media'; @@ -538,7 +540,9 @@ public function testCreateThrowsExceptionIfProvidedImageNameContainsForbiddenCha public function testUpdateThrowsExceptionIfTargetProductDoesNotExist() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The product that was requested doesn\'t exist. Verify the product and try again.'); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); $this->updateServiceInfo['rest']['resourcePath'] = '/V1/products/wrong_product_sku/media' . '/' . 'wrong-sku'; @@ -592,7 +596,9 @@ public function testUpdateThrowsExceptionIfThereIsNoImageWithGivenId() public function testDeleteThrowsExceptionIfTargetProductDoesNotExist() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The product that was requested doesn\'t exist. Verify the product and try again.'); + $this->expectExceptionMessage( + 'The product that was requested doesn\'t exist. Verify the product and try again.' + ); $this->deleteServiceInfo['rest']['resourcePath'] = '/V1/products/wrong_product_sku/media/9999'; $requestData = [ @@ -782,6 +788,6 @@ public function testAddProductVideo() $this->assertEquals(1, $updatedImage['position']); $this->assertEquals(0, $updatedImage['disabled']); $this->assertStringStartsWith('/t/e/test_image', $updatedImage['file']); - $this->assertEquals($videoContent, array_intersect($updatedImage, $videoContent)); + $this->assertEquals($videoContent, array_intersect_key($updatedImage, $videoContent)); } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 1b18949b0ac5b..d77737edadafa 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -25,6 +25,7 @@ use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\StoreWebsiteRelationInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreRepository; use Magento\Store\Model\Website; @@ -254,6 +255,59 @@ public function testUpdateWithDeleteWebsites() ); } + /** + * Test removing association between product and website 1 then check url rewrite removed + * Assign website back and check rewrite generated + * + * @magentoApiDataFixture Magento/Catalog/_files/product_two_websites.php + */ + public function testUpdateRewriteWithChangeWebsites() + { + /** @var Website $website */ + $website = $this->loadWebsiteByCode('test'); + + $productBuilder[ProductInterface::SKU] = 'simple-on-two-websites'; + $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = [ + 'website_ids' => [ + $website->getId(), + ], + ]; + $objectManager = Bootstrap::getObjectManager(); + /** @var StoreWebsiteRelationInterface $storeWebsiteRelation */ + $storeWebsiteRelation = $objectManager->get(StoreWebsiteRelationInterface::class); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + + $baseWebsite = $this->loadWebsiteByCode('base'); + $storeIds = $storeWebsiteRelation->getStoreByWebsiteId($baseWebsite->getId()); + $product = $productRepository->get($productBuilder[ProductInterface::SKU], false, reset($storeIds)); + $this->assertStringContainsString( + $product->getUrlKey() . '.html', + $product->getProductUrl() + ); + + $this->updateProduct($productBuilder); + + $product->setRequestPath(''); + $this->assertStringNotContainsString( + $product->getUrlKey() . '.html', + $product->getProductUrl() + ); + $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = [ + 'website_ids' => [ + $website->getId(), + $baseWebsite->getId(), + ], + ]; + + $this->updateProduct($productBuilder); + $product->setRequestPath(''); + $this->assertStringContainsString( + $product->getUrlKey() . '.html', + $product->getProductUrl() + ); + } + /** * Test removing all website associations * @@ -264,7 +318,7 @@ public function testDeleteAllWebsiteAssociations() $productBuilder[ProductInterface::SKU] = 'unique-simple-azaza'; $websitesData = [ - 'website_ids' => [] + 'website_ids' => [], ]; $productBuilder[ProductInterface::EXTENSION_ATTRIBUTES_KEY] = $websitesData; $response = $this->updateProduct($productBuilder); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php index fc0fdcf71525f..cb62cc0f75264 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php @@ -97,6 +97,7 @@ public function testAddBundleProductToCart() cart { items { id + uid quantity product { sku @@ -104,10 +105,12 @@ public function testAddBundleProductToCart() ... on BundleCartItem { bundle_options { id + uid label type values { id + uid label price quantity @@ -189,6 +192,7 @@ public function testAddBundleToCartWithWrongBundleOptions() cart { items { id + uid quantity product { sku @@ -196,10 +200,12 @@ public function testAddBundleToCartWithWrongBundleOptions() ... on BundleCartItem { bundle_options { id + uid label type values { id + uid label price quantity @@ -268,6 +274,7 @@ private function getProductQuery(string $sku): string items { sku option_id + uid required type title @@ -279,8 +286,8 @@ private function getProductQuery(string $sku): string } can_change_quantity id + uid price - quantity } } @@ -322,6 +329,7 @@ private function getMutationsQuery( cart { items { id + uid quantity product { sku @@ -329,10 +337,12 @@ private function getMutationsQuery( ... on BundleCartItem { bundle_options { id + uid label type values { id + uid label price quantity diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php index f705195050843..2c6d3af69bf40 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php @@ -104,6 +104,7 @@ public function testAddBundleProductToCart() cart { items { id + uid quantity product { sku @@ -111,10 +112,12 @@ public function testAddBundleProductToCart() ... on BundleCartItem { bundle_options { id + uid label type values { id + uid label price quantity diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php index a18b6e1206895..bc3fcd3a8427b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php @@ -38,7 +38,6 @@ public function testAllFieldsBundleProducts() type_id id name - attribute_set_id ... on PhysicalProductInterface { weight } @@ -54,7 +53,7 @@ public function testAllFieldsBundleProducts() required type position - sku + sku options { id quantity @@ -74,7 +73,7 @@ public function testAllFieldsBundleProducts() } } } - } + } } QUERY; @@ -118,7 +117,6 @@ public function testBundleProductWithNotVisibleChildren() type_id id name - attribute_set_id ... on PhysicalProductInterface { weight } @@ -134,7 +132,7 @@ public function testBundleProductWithNotVisibleChildren() required type position - sku + sku options { id quantity @@ -154,7 +152,7 @@ public function testBundleProductWithNotVisibleChildren() } } } - } + } } QUERY; @@ -207,8 +205,7 @@ private function assertBundleBaseFields($product, $actualResponse) ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], ['response_field' => 'id', 'expected_value' => $product->getId()], ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'weight', 'expected_value' => $product->getWeight()], + ['response_field' => 'weight', 'expected_value' => $product->getWeight()], ['response_field' => 'dynamic_price', 'expected_value' => !(bool)$product->getPriceType()], ['response_field' => 'dynamic_weight', 'expected_value' => !(bool)$product->getWeightType()], ['response_field' => 'dynamic_sku', 'expected_value' => !(bool)$product->getSkuType()] diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index 01777cfbfd694..18f21bcbc2ee5 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -32,6 +32,7 @@ public function testFilterSingleCategoryByField($field, $condition, $value, $exp categories(filters: { $field : { $condition : "$value" } }){ items{ id + uid name url_key url_path @@ -63,6 +64,7 @@ public function testFilterMultipleCategoriesByField($field, $condition, $value, categories(filters: { $field : { $condition : $value } }){ items{ id + uid name url_key url_path @@ -92,6 +94,7 @@ public function testFilterCategoryByMultipleFields() total_count items{ id + uid name url_key url_path @@ -122,6 +125,7 @@ public function testFilterWithInactiveCategory() categories(filters: {url_key: {in: ["inactive", "category-2"]}}){ items{ id + uid name url_key url_path @@ -147,6 +151,7 @@ public function testQueryChildCategoriesWithProducts() categories(filters: {ids: {in: ["3"]}}){ items{ id + uid name url_key url_path @@ -233,6 +238,7 @@ public function testQueryCategoryWithDisabledChildren() categories(filters: {ids: {in: ["3"]}}){ items{ id + uid name image url_key @@ -315,6 +321,7 @@ public function testNoResultsFound() categories(filters: {url_key: {in: ["inactive", "does-not-exist"]}}){ items{ id + uid name url_key url_path @@ -343,6 +350,7 @@ public function testEmptyFiltersReturnRootCategory() categories{ items{ id + uid name url_key url_path @@ -378,6 +386,7 @@ public function testMinimumMatchQueryLength() categories(filters: {name: {match: "mo"}}){ items{ id + uid name url_key url_path @@ -412,6 +421,7 @@ public function testCategoryImageNameAndSeoDisabled() categories(filters: {ids: {in: ["$categoryId"]}}) { items{ id + uid name image } @@ -444,6 +454,7 @@ public function testFilterByUrlPathTopLevelCategory() categories(filters: {url_path: {eq: "$urlPath"}}){ items{ id + uid name url_key url_path @@ -473,6 +484,7 @@ public function testFilterByUrlPathNestedCategory() categories(filters: {url_path: {eq: "$urlPath"}}){ items{ id + uid name url_key url_path @@ -503,6 +515,7 @@ public function testFilterByUrlPathMultipleCategories() categories(filters: {url_path: {in: [$urlPathsString]}}){ items{ id + uid name url_key url_path @@ -533,6 +546,7 @@ public function testFilterByUrlPathNoResults() categories(filters: {url_path: {in: ["not-a-category url path"]}}){ items{ id + uid name url_key url_path @@ -561,6 +575,22 @@ public function filterSingleCategoryDataProvider(): array '4', [ 'id' => '4', + 'uid' => base64_encode('4'), + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ] + ], + [ + 'category_uid', + 'eq', + base64_encode('4'), + [ + 'id' => '4', + 'uid' => base64_encode('4'), 'name' => 'Category 1.1', 'url_key' => 'category-1-1', 'url_path' => 'category-1/category-1-1', @@ -589,6 +619,7 @@ public function filterSingleCategoryDataProvider(): array 'Movable Position 2', [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -603,6 +634,7 @@ public function filterSingleCategoryDataProvider(): array 'category-1-1-1', [ 'id' => '5', + 'uid' => base64_encode('5'), 'name' => 'Category 1.1.1', 'url_key' => 'category-1-1-1', 'url_path' => 'category-1/category-1-1/category-1-1-1', @@ -656,6 +688,44 @@ public function filterMultipleCategoriesDataProvider(): array ] ] ], + //Filter by multiple UIDs + [ + 'category_uid', + 'in', + '["' . base64_encode('4') . '", "' . base64_encode('9') . '", "' . base64_encode('10') . '"]', + [ + [ + 'id' => '4', + 'uid' => base64_encode('4'), + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ], + [ + 'id' => '9', + 'uid' => base64_encode('9'), + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'uid' => base64_encode('10'), + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ] + ], // Filter by multiple parent IDs [ 'parent_id', @@ -699,6 +769,7 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '13', + 'uid' => base64_encode('13'), 'name' => 'Category 1.2', 'url_key' => 'category-1-2', 'url_path' => 'category-1/category-1-2', @@ -708,6 +779,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '7', + 'uid' => base64_encode('7'), 'name' => 'Movable', 'url_key' => 'movable', 'url_path' => 'movable', @@ -725,6 +797,7 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '9', + 'uid' => base64_encode('9'), 'name' => 'Movable Position 1', 'url_key' => 'movable-position-1', 'url_path' => 'movable-position-1', @@ -734,6 +807,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -743,6 +817,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '11', + 'uid' => base64_encode('11'), 'name' => 'Movable Position 3', 'url_key' => 'movable-position-3', 'url_path' => 'movable-position-3', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryAggregationsTest.php new file mode 100644 index 0000000000000..a0f184507a9aa --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryAggregationsTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog\CategoriesQuery; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test to return category aggregations + */ +class CategoryAggregationsTest extends GraphQlAbstract +{ + /** + * Test to return category aggregations in sorting by position + * + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attributes.php + */ + public function testCategoryAggregationSorting(): void + { + $categoryId = 3334; + $query = <<<QUERY +{ + products(filter: {category_id: {eq: "{$categoryId}"}}) { + aggregations{ + label + attribute_code + count + options{ + label + value + count + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $response); + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('aggregations', $response['products']); + + $customAggregation = array_values(array_filter( + $response['products']['aggregations'], + function ($a) { + return in_array($a['attribute_code'], ['test_attribute_1', 'test_attribute_2']); + } + )); + $this->assertCount(2, $customAggregation); + $this->assertEquals('test_attribute_2', $customAggregation[0]['attribute_code']); + $this->assertEquals('test_attribute_1', $customAggregation[1]['attribute_code']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php index dbbeaebc15936..8a483b5b12605 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php @@ -308,9 +308,7 @@ public function testCategoryProducts() page_size } items { - attribute_set_id country_of_manufacture - created_at description { html } @@ -349,8 +347,6 @@ public function testCategoryProducts() } } name - new_from_date - new_to_date options_container price { minimalPrice { @@ -409,7 +405,6 @@ public function testCategoryProducts() sku small_image { url, label } thumbnail { url, label } - special_from_date special_price special_to_date swatch_image @@ -422,17 +417,8 @@ public function testCategoryProducts() website_id } type_id - updated_at url_key url_path - websites { - id - name - code - sort_order - default_group_id - is_default - } } } } @@ -453,7 +439,6 @@ public function testCategoryProducts() $firstProductModel = $productRepository->get($firstProduct['sku'], false, null, true); $this->assertBaseFields($firstProductModel, $firstProduct); $this->assertAttributes($firstProduct); - $this->assertWebsites($firstProductModel, $firstProduct['websites']); $this->assertEquals('Category 1', $firstProduct['categories'][0]['name']); $this->assertEquals('category-1/category-1-1', $firstProduct['categories'][1]['url_path']); $this->assertCount(3, $firstProduct['categories']); @@ -636,6 +621,37 @@ public function testCategoryImage(?string $imagePrefix) $this->assertEquals($expectedImageUrl, $childCategory['image']); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testGetCategoryWithIdAndUid() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('`ids` and `category_uid` can\'t be used at the same time'); + + $categoryId = 8; + $categoryUid = base64_encode((string) 8); + $query = <<<QUERY +{ +categories(filters: {ids: {in: ["$categoryId"]}, category_uid: {in: ["$categoryUid"]}}) { + items { + id + name + url_key + image + children { + id + name + url_key + image + } + } +} +} +QUERY; + $this->graphQlQuery($query); + } + /** * @return array */ @@ -664,8 +680,6 @@ public function categoryImageDataProvider(): array private function assertBaseFields($product, $actualResponse) { $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'price', 'expected_value' => [ 'minimalPrice' => [ @@ -693,30 +707,10 @@ private function assertBaseFields($product, $actualResponse) ], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ]; $this->assertResponseFields($actualResponse, $assertionMap); } - /** - * @param ProductInterface $product - * @param array $actualResponse - */ - private function assertWebsites($product, $actualResponse) - { - $assertionMap = [ - [ - 'id' => current($product->getExtensionAttributes()->getWebsiteIds()), - 'name' => 'Main Website', - 'code' => 'base', - 'sort_order' => 0, - 'default_group_id' => '1', - 'is_default' => true, - ] - ]; - $this->assertEquals($actualResponse, $assertionMap); - } - /** * @param array $actualResponse */ @@ -731,11 +725,8 @@ private function assertAttributes($actualResponse) 'short_description', 'country_of_manufacture', 'gift_message_available', - 'new_from_date', - 'new_to_date', 'options_container', 'special_price', - 'special_from_date', 'special_to_date', ]; foreach ($eavAttributes as $eavAttribute) { diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index 43612575a7dcb..7cb7dacf2b188 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -42,6 +42,7 @@ public function testFilterSingleCategoryByField($field, $condition, $value, $exp { categoryList(filters: { $field : { $condition : "$value" } }){ id + uid name url_key url_path @@ -71,6 +72,7 @@ public function testFilterMultipleCategoriesByField($field, $condition, $value, { categoryList(filters: { $field : { $condition : $value } }){ id + uid name url_key url_path @@ -337,6 +339,7 @@ public function testEmptyFiltersReturnRootCategory() { categoryList{ id + uid name url_key url_path @@ -354,6 +357,7 @@ public function testEmptyFiltersReturnRootCategory() $this->assertArrayHasKey('categoryList', $result); $this->assertEquals('Default Category', $result['categoryList'][0]['name']); $this->assertEquals($storeRootCategoryId, $result['categoryList'][0]['id']); + $this->assertEquals(base64_encode($storeRootCategoryId), $result['categoryList'][0]['uid']); } /** @@ -370,6 +374,7 @@ public function testMinimumMatchQueryLength() { categoryList(filters: {name: {match: "mo"}}){ id + uid name url_key url_path @@ -542,6 +547,22 @@ public function filterSingleCategoryDataProvider(): array '4', [ 'id' => '4', + 'uid' => base64_encode('4'), + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ] + ], + [ + 'category_uid', + 'eq', + base64_encode('4'), + [ + 'id' => '4', + 'uid' => base64_encode('4'), 'name' => 'Category 1.1', 'url_key' => 'category-1-1', 'url_path' => 'category-1/category-1-1', @@ -556,6 +577,7 @@ public function filterSingleCategoryDataProvider(): array 'Movable Position 2', [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -596,6 +618,45 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '4', + 'uid' => base64_encode('4'), + 'name' => 'Category 1.1', + 'url_key' => 'category-1-1', + 'url_path' => 'category-1/category-1-1', + 'children_count' => '0', + 'path' => '1/2/3/4', + 'position' => '1' + ], + [ + 'id' => '9', + 'uid' => base64_encode('9'), + 'name' => 'Movable Position 1', + 'url_key' => 'movable-position-1', + 'url_path' => 'movable-position-1', + 'children_count' => '0', + 'path' => '1/2/9', + 'position' => '5' + ], + [ + 'id' => '10', + 'uid' => base64_encode('10'), + 'name' => 'Movable Position 2', + 'url_key' => 'movable-position-2', + 'url_path' => 'movable-position-2', + 'children_count' => '0', + 'path' => '1/2/10', + 'position' => '6' + ] + ] + ], + //Filter by multiple UIDs + [ + 'category_uid', + 'in', + '["' . base64_encode('4') . '", "' . base64_encode('9') . '", "' . base64_encode('10') . '"]', + [ + [ + 'id' => '4', + 'uid' => base64_encode('4'), 'name' => 'Category 1.1', 'url_key' => 'category-1-1', 'url_path' => 'category-1/category-1-1', @@ -605,6 +666,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '9', + 'uid' => base64_encode('9'), 'name' => 'Movable Position 1', 'url_key' => 'movable-position-1', 'url_path' => 'movable-position-1', @@ -614,6 +676,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -631,6 +694,7 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '13', + 'uid' => base64_encode('13'), 'name' => 'Category 1.2', 'url_key' => 'category-1-2', 'url_path' => 'category-1/category-1-2', @@ -640,6 +704,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '7', + 'uid' => base64_encode('7'), 'name' => 'Movable', 'url_key' => 'movable', 'url_path' => 'movable', @@ -657,6 +722,7 @@ public function filterMultipleCategoriesDataProvider(): array [ [ 'id' => '9', + 'uid' => base64_encode('9'), 'name' => 'Movable Position 1', 'url_key' => 'movable-position-1', 'url_path' => 'movable-position-1', @@ -666,6 +732,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '10', + 'uid' => base64_encode('10'), 'name' => 'Movable Position 2', 'url_key' => 'movable-position-2', 'url_path' => 'movable-position-2', @@ -675,6 +742,7 @@ public function filterMultipleCategoriesDataProvider(): array ], [ 'id' => '11', + 'uid' => base64_encode('11'), 'name' => 'Movable Position 3', 'url_key' => 'movable-position-3', 'url_path' => 'movable-position-3', @@ -725,6 +793,7 @@ public function testFilterCategoryInlineFragment() categoryList(filters: {ids: {eq: "6"}}){ ... on CategoryTree { id + uid name url_key url_path @@ -739,6 +808,7 @@ public function testFilterCategoryInlineFragment() $this->assertArrayNotHasKey('errors', $result); $this->assertCount(1, $result['categoryList']); $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['uid'], base64_encode('6')); $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); } @@ -756,6 +826,7 @@ public function testFilterCategoryNamedFragment() fragment Cat on CategoryTree { id + uid name url_key url_path @@ -768,6 +839,7 @@ public function testFilterCategoryNamedFragment() $this->assertArrayNotHasKey('errors', $result); $this->assertCount(1, $result['categoryList']); $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['uid'], base64_encode('6')); $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); } } 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 72b014fd39f0e..a77eccf5c623f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -333,9 +333,7 @@ public function testCategoryProducts() page_size } items { - attribute_set_id country_of_manufacture - created_at description { html } @@ -374,8 +372,6 @@ public function testCategoryProducts() } } name - new_from_date - new_to_date options_container price { minimalPrice { @@ -434,7 +430,6 @@ public function testCategoryProducts() sku small_image { url, label } thumbnail { url, label } - special_from_date special_price special_to_date swatch_image @@ -447,17 +442,8 @@ public function testCategoryProducts() website_id } type_id - updated_at url_key url_path - websites { - id - name - code - sort_order - default_group_id - is_default - } } } } @@ -478,7 +464,6 @@ public function testCategoryProducts() $firstProduct = $productRepository->get($firstProductSku, false, null, true); $this->assertBaseFields($firstProduct, $response['category']['products']['items'][0]); $this->assertAttributes($response['category']['products']['items'][0]); - $this->assertWebsites($firstProduct, $response['category']['products']['items'][0]['websites']); } /** @@ -541,6 +526,7 @@ public function testBreadCrumbs() name breadcrumbs { category_id + category_uid category_name category_level category_url_key @@ -556,6 +542,7 @@ public function testBreadCrumbs() 'breadcrumbs' => [ [ 'category_id' => 3, + 'category_uid' => base64_encode('3'), 'category_name' => "Category 1", 'category_level' => 2, 'category_url_key' => "category-1", @@ -563,6 +550,7 @@ public function testBreadCrumbs() ], [ 'category_id' => 4, + 'category_uid' => base64_encode('4'), 'category_name' => "Category 1.1", 'category_level' => 3, 'category_url_key' => "category-1-1", @@ -679,6 +667,7 @@ public function testBreadCrumbsWithDisabledParentCategory() name breadcrumbs { category_id + category_uid category_name } } @@ -691,6 +680,7 @@ public function testBreadCrumbsWithDisabledParentCategory() 'breadcrumbs' => [ [ 'category_id' => 3, + 'category_uid' => base64_encode('3'), 'category_name' => "Category 1", ] ] @@ -727,8 +717,6 @@ public function categoryImageDataProvider(): array private function assertBaseFields($product, $actualResponse) { $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'price', 'expected_value' => [ 'minimalPrice' => [ @@ -756,30 +744,10 @@ private function assertBaseFields($product, $actualResponse) ], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ]; $this->assertResponseFields($actualResponse, $assertionMap); } - /** - * @param ProductInterface $product - * @param array $actualResponse - */ - private function assertWebsites($product, $actualResponse) - { - $assertionMap = [ - [ - 'id' => current($product->getExtensionAttributes()->getWebsiteIds()), - 'name' => 'Main Website', - 'code' => 'base', - 'sort_order' => 0, - 'default_group_id' => '1', - 'is_default' => true, - ] - ]; - $this->assertEquals($actualResponse, $assertionMap); - } - /** * @param array $actualResponse */ @@ -794,12 +762,8 @@ private function assertAttributes($actualResponse) 'short_description', 'country_of_manufacture', 'gift_message_available', - 'new_from_date', - 'new_to_date', 'options_container', - 'special_price', - 'special_from_date', - 'special_to_date', + 'special_price' ]; foreach ($eavAttributes as $eavAttribute) { $this->assertArrayHasKey($eavAttribute, $actualResponse); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php index b19b8d519e857..ab04333de0740 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php @@ -39,7 +39,7 @@ public function testQuerySimpleProductAfterDelete() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id + id } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php index d17b434f39d9f..7c7212b9b9b26 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductInMultipleStoresTest.php @@ -11,7 +11,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Class ProductInMultipleStoresTest + * The GraphQl test for product in multiple stores */ class ProductInMultipleStoresTest extends GraphQlAbstract { @@ -31,8 +31,6 @@ public function testProductFromSpecificAndDefaultStore() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -45,7 +43,6 @@ public function testProductFromSpecificAndDefaultStore() } sku type_id - updated_at ... on PhysicalProductInterface { weight } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php index b6c4b55dc1d23..ceb52354fa6eb 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php @@ -15,6 +15,8 @@ use Magento\ConfigurableProduct\Api\LinkManagementInterface; use Magento\ConfigurableProduct\Model\LinkManagement; use Magento\Customer\Model\Group; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\GraphQl\Customer\LockCustomer; use Magento\Framework\ObjectManager\ObjectManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -27,11 +29,23 @@ class ProductPriceTest extends GraphQlAbstract /** @var ProductRepositoryInterface $productRepository */ private $productRepository; + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var LockCustomer + */ + private $lockCustomer; + protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); /** @var ProductRepositoryInterface $productRepository */ $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + $this->lockCustomer = $this->objectManager->get(LockCustomer::class); } /** @@ -235,10 +249,20 @@ public function testMultipleProductTypes() * Simple products with special price and tier price with % discount * * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @param int $customerGroup + * @param array $expectedPriceRange + * @param array $expectedTierPrices + * @param array $customerData + * @param bool $isTierPriceExists + * @dataProvider priceDataProvider */ - public function testSimpleProductsWithSpecialPriceAndTierPrice() - { + public function testSimpleProductsWithSpecialPriceAndTierPrice( + int $customerGroup, + array $expectedPriceRange, + array $expectedTierPrices, + array $customerData + ) { $skus = ["simple1", "simple2"]; $tierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); @@ -249,7 +273,7 @@ public function testSimpleProductsWithSpecialPriceAndTierPrice() $tierPrices[] = $tierPriceFactory->create( [ 'data' => [ - 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'customer_group_id' => $customerGroup, 'qty' => 2 ] ] @@ -260,97 +284,137 @@ public function testSimpleProductsWithSpecialPriceAndTierPrice() $simpleProduct->setTierPrices($tierPrices); $this->productRepository->save($simpleProduct); } + + $headerMap = []; + if (!empty($customerData)) { + $customerToken = $this->customerTokenService->createCustomerAccessToken( + $customerData['username'], + $customerData['password'] + ); + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + } + $query = $this->getProductQuery($skus); - $result = $this->graphQlQuery($query); + $result = $this->graphQlQuery($query, [], '', $headerMap); $this->assertArrayNotHasKey('errors', $result); $this->assertCount(2, $result['products']['items']); - $expectedPriceRange = [ - "simple1" => [ - "minimum_price" => [ - "regular_price" => [ - "value" => 10 - ], - "final_price" => [ - "value" => 5.99 + foreach ($result['products']['items'] as $product) { + $this->assertNotEmpty($product['price_range']); + $this->assertNotEmpty($product['price_tiers']); + $this->assertPrices($expectedPriceRange[$product['sku']], $product['price_range']); + $this->assertResponseFields($product['price_tiers'], $expectedTierPrices[$product['sku']]); + } + } + + /** + * Data provider for prices + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function priceDataProvider() : array + { + return [ + [ + 'customer_group' => Group::CUST_GROUP_ALL, + 'expected_price_range' => [ + "simple1" => [ + "minimum_price" => [ + "regular_price" => ["value" => 10], + "final_price" => ["value" => 5.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 40.1] + ], + "maximum_price" => [ + "regular_price" => ["value" => 10], + "final_price" => ["value" => 5.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 40.1] + ] ], - "discount" => [ - "amount_off" => 4.01, - "percent_off" => 40.1 + "simple2" => [ + "minimum_price" => [ + "regular_price" => ["value" => 20], + "final_price" => ["value" => 15.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 20.05] + ], + "maximum_price" => [ + "regular_price" => ["value" => 20], + "final_price" => ["value" => 15.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 20.05] + ] ] ], - "maximum_price" => [ - "regular_price" => [ - "value" => 10 + 'expected_tier_prices' => [ + "simple1" => [ + 0 => [ + 'discount' =>['amount_off' => 1, 'percent_off' => 10], + 'final_price' =>['value'=> 9], + 'quantity' => 2 + ] ], - "final_price" => [ - "value" => 5.99 - ], - "discount" => [ - "amount_off" => 4.01, - "percent_off" => 40.1 + "simple2" => [ + 0 => [ + 'discount' =>['amount_off' => 2, 'percent_off' => 10], + 'final_price' =>['value'=> 18], + 'quantity' => 2 + ] ] - ] + ], + 'customer_data' => [] ], - "simple2" => [ - "minimum_price" => [ - "regular_price" => [ - "value" => 20 - ], - "final_price" => [ - "value" => 15.99 + [ + 'customer_group' => 1, + 'expected_price_range' => [ + "simple1" => [ + "minimum_price" => [ + "regular_price" => ["value" => 10], + "final_price" => ["value" => 5.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 40.1] + ], + "maximum_price" => [ + "regular_price" => ["value" => 10], + "final_price" => ["value" => 5.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 40.1] + ] ], - "discount" => [ - "amount_off" => 4.01, - "percent_off" => 20.05 + "simple2" => [ + "minimum_price" => [ + "regular_price" => ["value" => 20], + "final_price" => ["value" => 15.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 20.05] + ], + "maximum_price" => [ + "regular_price" => ["value" => 20], + "final_price" => ["value" => 15.99], + "discount" => ["amount_off" => 4.01, "percent_off" => 20.05] + ] ] ], - "maximum_price" => [ - "regular_price" => [ - "value" => 20 + 'expected_tier_prices' => [ + "simple1" => [ + 0 => [ + 'discount' =>['amount_off' => 1, 'percent_off' => 10], + 'final_price' =>['value'=> 9], + 'quantity' => 2 + ] ], - "final_price" => [ - "value" => 15.99 - ], - "discount" => [ - "amount_off" => 4.01, - "percent_off" => 20.05 + "simple2" => [ + 0 => [ + 'discount' =>['amount_off' => 2, 'percent_off' => 10], + 'final_price' =>['value'=> 18], + 'quantity' => 2 + ] ] - ] - ] - ]; - $expectedTierPrices = [ - "simple1" => [ - 0 => [ - 'discount' =>[ - 'amount_off' => 1, - 'percent_off' => 10 - ], - 'final_price' =>['value'=> 9], - 'quantity' => 2 + ], + 'customer_data' => [ + 'username' => 'customer@example.com', + 'password' => 'password' ] ], - "simple2" => [ - 0 => [ - 'discount' =>[ - 'amount_off' => 2, - 'percent_off' => 10 - ], - 'final_price' =>['value'=> 18], - 'quantity' => 2 - ] - - ] ]; - - foreach ($result['products']['items'] as $product) { - $this->assertNotEmpty($product['price_range']); - $this->assertNotEmpty($product['price_tiers']); - $this->assertPrices($expectedPriceRange[$product['sku']], $product['price_range']); - $this->assertResponseFields($product['price_tiers'], $expectedTierPrices[$product['sku']]); - } } + /** * Check the pricing for a grouped product with simple products having special price set * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 7b14fe9159c57..6c64539e38cb2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -65,6 +65,25 @@ public function testFilterForNonExistingCategory() ); } + /** + * Verify that filters id and uid can't be used at the same time + */ + public function testUidAndIdUsageErrorOnProductFilteringCategory() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('`category_id` and `category_uid` can\'t be used at the same time'); + $query = <<<QUERY +{ + products(filter: {category_id: {eq: "99999999"}, category_uid: {eq: "OTk5OTk5OTk="}}) { + filters { + name + } + } +} +QUERY; + $this->graphQlQuery($query); + } + /** * Verify that layered navigation filters and aggregations are correct for product query * @@ -154,7 +173,6 @@ private function compareFilterNames(array $a, array $b) */ public function testLayeredNavigationForConfigurableProducts() { - CacheCleaner::cleanAll(); $attributeCode = 'test_configurable'; /** @var Config $eavConfig */ @@ -258,7 +276,7 @@ private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $fi */ public function testFilterProductsByDropDownCustomAttribute() { - CacheCleaner::cleanAll(); + CacheCleaner::clean(['eav']); $attributeCode = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attributeCode); $query = <<<QUERY @@ -607,10 +625,11 @@ public function testFilterByCategoryIdAndCustomAttribute() $getCategoryByName = Bootstrap::getObjectManager()->get(GetCategoryByName::class); $category = $getCategoryByName->execute('Category 1.2'); $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); + $categoryUid = base64_encode($category->getId()); $query = <<<QUERY { products(filter:{ - category_id : {eq:"{$category->getId()}"} + category_uid : {eq:"{$categoryUid}"} second_test_configurable: {eq: "{$optionValue}"} }, pageSize: 3 @@ -1081,7 +1100,6 @@ public function testFilterWithinSpecificPriceRangeSortedByNameDesc() weight } type_id - attribute_set_id } total_count page_info @@ -1234,7 +1252,6 @@ public function testSearchWithFilterWithPageSizeEqualTotalCount() weight } type_id - attribute_set_id } total_count page_info @@ -1298,7 +1315,6 @@ public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields() weight } type_id - attribute_set_id } total_count page_info @@ -1349,7 +1365,6 @@ public function testFilterByMultipleFilterFieldsSortedByMultipleSortFields() */ public function testFilterProductsForExactMatchingName() { - $query = <<<QUERY { @@ -1502,17 +1517,20 @@ public function testFilteringForProductsFromMultipleCategories() * * @magentoApiDataFixture Magento/Catalog/_files/product_in_multiple_categories.php * @return void + * @dataProvider filterProductsBySingleCategoryIdDataProvider */ - public function testFilterProductsBySingleCategoryId() + public function testFilterProductsBySingleCategoryId(string $fieldName, string $queryCategoryId) { - $queryCategoryId = 333; + if (is_numeric($queryCategoryId)) { + $queryCategoryId = (int) $queryCategoryId; + } $query = <<<QUERY { products( filter: { - category_id:{eq:"{$queryCategoryId}"} + {$fieldName}:{eq:"{$queryCategoryId}"} } pageSize:2 @@ -1526,6 +1544,7 @@ public function testFilterProductsBySingleCategoryId() categories{ name id + uid path children_count product_count @@ -1545,7 +1564,9 @@ public function testFilterProductsBySingleCategoryId() /** @var CategoryRepositoryInterface $categoryRepository */ $categoryRepository = ObjectManager::getInstance()->get(CategoryRepositoryInterface::class); - $links = $productLinks->getAssignedProducts($queryCategoryId); + $links = $productLinks->getAssignedProducts( + is_numeric($queryCategoryId) ? $queryCategoryId : base64_decode($queryCategoryId) + ); $links = array_reverse($links); foreach ($response['products']['items'] as $itemIndex => $itemData) { $this->assertNotEmpty($itemData); @@ -1574,6 +1595,7 @@ public function testFilterProductsBySingleCategoryId() [ 'name' => $category->getName(), 'id' => $category->getId(), + 'uid' => base64_encode($category->getId()), 'path' => $category->getPath(), 'children_count' => $category->getChildrenCount(), 'product_count' => $category->getProductCount(), @@ -1693,7 +1715,6 @@ public function testFilterByExactSkuAndSortByPriceDesc() weight } type_id - attribute_set_id } total_count page_info @@ -2119,7 +2140,6 @@ public function testFilterWithinASpecificPriceRangeSortedByPriceDESC() { items { - attribute_set_id sku price { minimalPrice { @@ -2219,7 +2239,6 @@ public function testQueryFilterNoMatchingItems() weight } type_id - attribute_set_id } total_count page_info @@ -2276,7 +2295,6 @@ public function testQueryPageOutOfBoundException() ... on PhysicalProductInterface { weight } - attribute_set_id } total_count page_info @@ -2309,12 +2327,9 @@ public function testQueryWithNoSearchOrFilterArgumentException() { items{ id - attribute_set_id - created_at name sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -2457,7 +2472,6 @@ private function assertProductItems(array $filteredProducts, array $actualRespon $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], [ - 'attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), 'sku' => $filteredProducts[$itemIndex]->getSku(), 'name' => $filteredProducts[$itemIndex]->getName(), 'price' => [ @@ -2484,7 +2498,6 @@ private function assertProductItemsWithPriceCheck(array $filteredProducts, array $this->assertResponseFields( $productItemsInResponse[$itemIndex][0], [ - 'attribute_set_id' => $filteredProducts[$itemIndex]->getAttributeSetId(), 'sku' => $filteredProducts[$itemIndex]->getSku(), 'name' => $filteredProducts[$itemIndex]->getName(), 'price' => [ @@ -2514,4 +2527,23 @@ private function assertProductItemsWithPriceCheck(array $filteredProducts, array ); } } + + /** + * Data provider for product single category filtering + * + * @return array[][] + */ + public function filterProductsBySingleCategoryIdDataProvider(): array + { + return [ + [ + 'fieldName' => 'category_id', + 'categoryId' => '333', + ], + [ + 'fieldName' => 'category_uid', + 'categoryId' => base64_encode('333'), + ], + ]; + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index 9946e74a24994..d573e2893e8f3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -45,11 +45,10 @@ public function testQueryAllFieldsSimpleProduct() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id country_of_manufacture - created_at gift_message_available id + uid categories { name url_path @@ -65,6 +64,7 @@ public function testQueryAllFieldsSimpleProduct() disabled file id + uid label media_type position @@ -86,11 +86,10 @@ public function testQueryAllFieldsSimpleProduct() } } name - new_from_date - new_to_date options_container ... on CustomizableProductInterface { options { + uid title required sort_order @@ -229,10 +228,9 @@ public function testQueryAllFieldsSimpleProduct() sku small_image{ url, label } thumbnail { url, label } - special_from_date special_price special_to_date - swatch_image + swatch_image tier_price tier_prices { @@ -243,11 +241,9 @@ public function testQueryAllFieldsSimpleProduct() website_id } type_id - updated_at url_key url_path canonical_url - websites { id name code sort_order default_group_id is_default } ... on PhysicalProductInterface { weight } @@ -276,8 +272,6 @@ public function testQueryAllFieldsSimpleProduct() $this->assertBaseFields($product, $response['products']['items'][0]); $this->assertEavAttributes($product, $response['products']['items'][0]); $this->assertOptions($product, $response['products']['items'][0]); - $this->assertArrayHasKey('websites', $response['products']['items'][0]); - $this->assertWebsites($product, $response['products']['items'][0]['websites']); self::assertEquals( 'Movable Position 2', $responseObject->getData('products/items/0/categories/0/name') @@ -303,15 +297,15 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() products(filter: {sku: {eq: "{$productSku}"}}) { items{ - attribute_set_id categories { id + uid } country_of_manufacture - created_at gift_message_available id + uid image {url, label} meta_description meta_keyword @@ -321,6 +315,7 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() disabled file id + uid label media_type position @@ -342,8 +337,6 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() } } name - new_from_date - new_to_date options_container ... on CustomizableProductInterface { field_options: options { @@ -351,6 +344,7 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() required sort_order option_id + uid ... on CustomizableFieldOption { product_sku field_option: value { @@ -462,7 +456,6 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() } sku small_image { url, label } - special_from_date special_price special_to_date swatch_image @@ -477,10 +470,8 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() website_id } type_id - updated_at url_key url_path - websites { id name code sort_order default_group_id is_default } ... on PhysicalProductInterface { weight } @@ -501,8 +492,6 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct() $this->assertCount(1, $response['products']['items']); $this->assertArrayHasKey(0, $response['products']['items']); $this->assertMediaGalleryEntries($product, $response['products']['items'][0]); - $this->assertArrayHasKey('websites', $response['products']['items'][0]); - $this->assertWebsites($product, $response['products']['items'][0]['websites']); } /** @@ -548,7 +537,6 @@ public function testProductLinks() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id type_id product_links { @@ -585,9 +573,8 @@ public function testProductPrices() products(filter: {price: {from: "150.0", to: "250.0"}}) { items { - attribute_set_id - created_at id + uid name price { minimalPrice { @@ -635,7 +622,6 @@ public function testProductPrices() } sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -679,6 +665,7 @@ private function assertMediaGalleryEntries($product, $actualResponse) 'disabled' => (bool)$mediaGalleryEntry->isDisabled(), 'file' => $mediaGalleryEntry->getFile(), 'id' => $mediaGalleryEntry->getId(), + 'uid' => base64_encode($mediaGalleryEntry->getId()), 'label' => $mediaGalleryEntry->getLabel(), 'media_type' => $mediaGalleryEntry->getMediaType(), 'position' => $mediaGalleryEntry->getPosition(), @@ -744,7 +731,11 @@ private function assertOptions($product, $actualResponse) ['response_field' => 'sort_order', 'expected_value' => $option->getSortOrder()], ['response_field' => 'title', 'expected_value' => $option->getTitle()], ['response_field' => 'required', 'expected_value' => $option->getIsRequire()], - ['response_field' => 'option_id', 'expected_value' => $option->getOptionId()] + ['response_field' => 'option_id', 'expected_value' => $option->getOptionId()], + [ + 'response_field' => 'uid', + 'expected_value' => base64_encode('custom-option/' . $option->getOptionId()) + ] ]; if (!empty($option->getValues())) { @@ -813,14 +804,11 @@ private function assertOptions($product, $actualResponse) */ private function assertBaseFields($product, $actualResponse) { - $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'id', 'expected_value' => $product->getId()], + ['response_field' => 'uid', 'expected_value' => base64_encode($product->getId())], ['response_field' => 'name', 'expected_value' => $product->getName()], - ['response_field' => 'price', 'expected_value' => - [ + ['response_field' => 'price', 'expected_value' => [ 'minimalPrice' => [ 'amount' => [ 'value' => $product->getSpecialPrice(), @@ -846,33 +834,12 @@ private function assertBaseFields($product, $actualResponse) ], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ['response_field' => 'weight', 'expected_value' => $product->getWeight()], ]; $this->assertResponseFields($actualResponse, $assertionMap); } - /** - * @param ProductInterface $product - * @param array $actualResponse - */ - private function assertWebsites($product, $actualResponse) - { - $assertionMap = [ - [ - 'id' => current($product->getExtensionAttributes()->getWebsiteIds()), - 'name' => 'Main Website', - 'code' => 'base', - 'sort_order' => 0, - 'default_group_id' => '1', - 'is_default' => true, - ] - ]; - - $this->assertEquals($actualResponse, $assertionMap); - } - /** * @param ProductInterface $product * @param array $actualResponse @@ -905,10 +872,8 @@ private function assertEavAttributes($product, $actualResponse) 'meta_title', 'country_of_manufacture', 'gift_message_available', - 'news_from_date', 'options_container', 'special_price', - 'special_from_date', 'special_to_date', ]; $assertionMap = []; @@ -930,14 +895,6 @@ private function assertEavAttributes($product, $actualResponse) */ private function eavAttributesToGraphQlSchemaFieldTranslator(string $eavAttributeCode) { - switch ($eavAttributeCode) { - case 'news_from_date': - $eavAttributeCode = 'new_from_date'; - break; - case 'news_to_date': - $eavAttributeCode = 'new_to_date'; - break; - } return $eavAttributeCode; } @@ -956,6 +913,7 @@ public function testProductInAllAnchoredCategories() name categories { id + uid name is_anchor } @@ -982,6 +940,7 @@ public function testProductInAllAnchoredCategories() [ 'name' => $category->getName(), 'id' => $category->getId(), + 'uid' => base64_encode($category->getId()), 'is_anchor' => $category->getIsAnchor() ] ); @@ -1006,6 +965,7 @@ public function testProductWithNonAnchoredParentCategory() name categories { id + uid name is_anchor } @@ -1037,6 +997,7 @@ public function testProductWithNonAnchoredParentCategory() [ 'name' => $category->getName(), 'id' => $category->getId(), + 'uid' => base64_encode($category->getId()), 'is_anchor' => $category->getIsAnchor() ] ); @@ -1054,7 +1015,7 @@ public function testProductInNonAnchoredSubCategories() $query = <<<QUERY { - products(filter: + products(filter: { sku: {in:["12345"]} } @@ -1066,6 +1027,7 @@ public function testProductInNonAnchoredSubCategories() name categories { id + uid name is_anchor } @@ -1098,6 +1060,7 @@ public function testProductInNonAnchoredSubCategories() [ 'name' => $category->getName(), 'id' => $category->getId(), + 'uid' => base64_encode($category->getId()), 'is_anchor' => $category->getIsAnchor() ] ); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php index 0982007daaa44..69c432f4cc82a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/StoreConfigTest.php @@ -42,6 +42,7 @@ public function testGetStoreConfig() list_per_page, catalog_default_sort_by, root_category_id + root_category_uid } } QUERY; @@ -58,6 +59,7 @@ public function testGetStoreConfig() $this->assertEquals(8, $response['storeConfig']['list_per_page']); $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); $this->assertEquals(2, $response['storeConfig']['root_category_id']); + $this->assertEquals(base64_encode('2'), $response['storeConfig']['root_category_uid']); } /** @@ -88,6 +90,7 @@ public function testGetStoreConfigGlobal() list_per_page, catalog_default_sort_by, root_category_id + root_category_uid } } QUERY; @@ -104,5 +107,6 @@ public function testGetStoreConfigGlobal() $this->assertEquals(8, $response['storeConfig']['list_per_page']); $this->assertEquals('asc', $response['storeConfig']['catalog_default_sort_by']); $this->assertEquals(2, $response['storeConfig']['root_category_id']); + $this->assertEquals(base64_encode('2'), $response['storeConfig']['root_category_uid']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php index 00f0c496d8ea4..0661ca04e3b49 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/VirtualProductViewTest.php @@ -30,21 +30,16 @@ public function testQueryAllFieldsVirtualProduct() { items{ id - attribute_set_id - created_at name sku type_id - updated_at ... on PhysicalProductInterface { weight - } + } ... on VirtualProduct { - attribute_set_id name id sku - } } } @@ -84,24 +79,20 @@ public function testCannotQueryWeightOnVirtualProductException() { items{ id - attribute_set_id - created_at name sku type_id - updated_at ... on PhysicalProductInterface { weight - } + } ... on VirtualProduct { - attribute_set_id name weight id - sku + sku } } - } + } } QUERY; @@ -119,7 +110,6 @@ public function testCannotQueryWeightOnVirtualProductException() private function assertBaseFields($product, $actualResponse) { $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], ['response_field' => 'id', 'expected_value' => $product->getId()], ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'sku', 'expected_value' => $product->getSku()], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php index b12630d70713a..e6c7851f775df 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogUrlRewrite/UrlResolverTest.php @@ -179,7 +179,6 @@ public function testRedirectsAndCustomInput() $actualUrls->getEntityType(), 0 ); - // querying a url that's a redirect the active redirected final url $this->queryUrlAndAssertResponse( (int) $product->getEntityId(), @@ -188,7 +187,6 @@ public function testRedirectsAndCustomInput() $actualUrls->getEntityType(), 301 ); - // create custom url that doesn't redirect /** @var UrlRewrite $urlRewriteModel */ $urlRewriteModel = $this->objectManager->create(UrlRewrite::class); @@ -209,7 +207,6 @@ public function testRedirectsAndCustomInput() $urlRewriteModel->setData($key, $value); } $urlRewriteModel->save(); - // querying a custom url that should return the target entity but relative should be the custom url $this->queryUrlAndAssertResponse( (int) $product->getEntityId(), @@ -218,18 +215,14 @@ public function testRedirectsAndCustomInput() $actualUrls->getEntityType(), 0 ); - // change custom url that does redirect $urlRewriteModel->setRedirectType('301'); $urlRewriteModel->setId($urlRewriteModel->getId()); $urlRewriteModel->save(); - - ObjectManager::getInstance()->get(\Magento\TestFramework\Helper\CacheCleaner::class)->cleanAll(); - //modifying query by adding spaces to avoid getting cached values. $this->queryUrlAndAssertResponse( (int) $product->getEntityId(), - $customUrl, + $customUrl . ' ', $actualUrls->getRequestPath(), strtoupper($actualUrls->getEntityType()), 301 @@ -242,10 +235,10 @@ public function testRedirectsAndCustomInput() * * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category.php */ - public function testCategoryUrlResolver() + public function testCategoryUrlResolver($categoryUrlPath = null) { $productSku = 'p002'; - $categoryUrlPath = 'cat-1.html'; + $categoryUrlPath = $categoryUrlPath ? $categoryUrlPath : 'cat-1.html'; /** @var ProductRepositoryInterface $productRepository */ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $product = $productRepository->get($productSku, false, null, true); @@ -363,6 +356,7 @@ public function testInvalidUrlResolverInput() urlResolver(url:"{$urlPath}") { id + entity_uid relative_url type redirectCode @@ -461,6 +455,26 @@ public function testGetNonExistentUrlRewrite() $this->assertEquals(0, $response['urlResolver']['redirectCode']); } + /** + * Test for category entity with empty url suffix + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix.php + */ + public function testCategoryUrlResolverWithEmptyUrlSuffix() + { + $this->testCategoryUrlResolver('cat-1'); + } + + /** + * Tests if target_path(relative_url) is resolved for Product entity with empty url suffix + * + * @magentoApiDataFixture Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix.php + */ + public function testProductUrlResolverWithEmptyUrlSuffix() + { + $this->testProductUrlResolver(); + } + /** * Assert response from GraphQl * @@ -483,6 +497,7 @@ private function queryUrlAndAssertResponse( urlResolver(url:"{$urlKey}") { id + entity_uid relative_url type redirectCode @@ -492,6 +507,7 @@ private function queryUrlAndAssertResponse( $response = $this->graphQlQuery($query); $this->assertArrayHasKey('urlResolver', $response); $this->assertEquals($productId, $response['urlResolver']['id']); + $this->assertEquals(base64_encode((string)$productId), $response['urlResolver']['entity_uid']); $this->assertEquals($relativePath, $response['urlResolver']['relative_url']); $this->assertEquals(strtoupper($expectedType), $response['urlResolver']['type']); $this->assertEquals($redirectCode, $response['urlResolver']['redirectCode']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CompareList/CompareListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CompareList/CompareListTest.php new file mode 100644 index 0000000000000..645012030f439 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CompareList/CompareListTest.php @@ -0,0 +1,427 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CompareList; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for Compare list feature + */ +class CompareListTest extends GraphQlAbstract +{ + private const PRODUCT_SKU_1 = 'simple1'; + private const PRODUCT_SKU_2 = 'simple2'; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + } + /** + * Create compare list without product + */ + public function testCreateCompareListWithoutProducts() + { + $response = $this->createCompareList(); + $uid = $response['createCompareList']['uid']; + $this->uidAssertion($uid); + } + + /** + * Create compare list with products + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testCreateCompareListWithProducts() + { + $product1 = $this->productRepository->get(self::PRODUCT_SKU_1); + $product2 = $this->productRepository->get(self::PRODUCT_SKU_2); + + $mutation = <<<MUTATION +mutation{ + createCompareList(input:{products: [{$product1->getId()}, {$product2->getId()}]}){ + uid + items { + product { + sku + } + } + } +} +MUTATION; + $response = $this->graphQlMutation($mutation); + $uid = $response['createCompareList']['uid']; + $this->uidAssertion($uid); + $this->itemsAssertion($response['createCompareList']['items']); + } + + /** + * Add products to compare list + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testAddProductToCompareList() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->assertEquals(0, $compareList['createCompareList']['item_count'],'Incorrect count'); + $this->uidAssertion($uid); + $response = $this->addProductsToCompareList($uid); + $resultUid = $response['addProductsToCompareList']['uid']; + $this->uidAssertion($resultUid); + $this->itemsAssertion($response['addProductsToCompareList']['items']); + $this->assertEquals(2, $response['addProductsToCompareList']['item_count'],'Incorrect count'); + $this->assertResponseFields( + $response['addProductsToCompareList']['attributes'], + [ + [ + 'code'=> 'sku', + 'label'=> 'SKU' + ], + [ + 'code'=> 'description', + 'label'=> 'Description' + ], + [ + 'code'=> 'short_description', + 'label'=> 'Short Description' + ] + ] + ); + } + + /** + * Remove products from compare list + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testRemoveProductFromCompareList() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->uidAssertion($uid); + $addProducts = $this->addProductsToCompareList($uid); + $this->itemsAssertion($addProducts['addProductsToCompareList']['items']); + $this->assertCount(2, $addProducts['addProductsToCompareList']['items']); + $product = $this->productRepository->get(self::PRODUCT_SKU_1); + $removeFromCompareList = <<<MUTATION +mutation{ + removeProductsFromCompareList(input: {uid: "{$uid}", products: [{$product->getId()}]}) { + uid + items { + product { + sku + } + } + } +} +MUTATION; + $response = $this->graphQlMutation($removeFromCompareList); + $this->assertCount(1, $response['removeProductsFromCompareList']['items']); + } + + /** + * Get compare list query + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testGetCompareList() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->uidAssertion($uid); + $addProducts = $this->addProductsToCompareList($uid); + $this->itemsAssertion($addProducts['addProductsToCompareList']['items']); + $query = <<<QUERY +{ + compareList(uid: "{$uid}") { + uid + items { + product { + sku + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->itemsAssertion($response['compareList']['items']); + } + + /** + * Remove compare list + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + */ + public function testDeleteCompareList() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->uidAssertion($uid); + $addProducts = $this->addProductsToCompareList($uid); + $this->itemsAssertion($addProducts['addProductsToCompareList']['items']); + $deleteCompareList = <<<MUTATION +mutation{ + deleteCompareList(uid:"{$uid}") { + result + } +} +MUTATION; + $response = $this->graphQlMutation($deleteCompareList); + $this->assertTrue($response['deleteCompareList']['result']); + $response1 = $this->graphQlMutation($deleteCompareList); + $this->assertFalse($response1['deleteCompareList']['result']); + } + + /** + * Assign compare list to customer + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testAssignCompareListToCustomer() + { + $compareList = $this->createCompareList(); + $uid = $compareList['createCompareList']['uid']; + $this->uidAssertion($uid); + $addProducts = $this->addProductsToCompareList($uid); + $this->itemsAssertion($addProducts['addProductsToCompareList']['items']); + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $customerQuery = <<<QUERY +{ + customer { + firstname + lastname + compare_list { + uid + items { + product { + sku + } + } + } + } +} +QUERY; + $customerResponse = $this->graphQlQuery( + $customerQuery, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('compare_list', $customerResponse['customer']); + $this->assertNull($customerResponse['customer']['compare_list']); + + $assignCompareListToCustomer = <<<MUTATION +mutation { + assignCompareListToCustomer(uid: "{$uid}"){ + result + compare_list { + uid + items { + uid + } + } + } +} +MUTATION; + $assignResponse = $this->graphQlMutation( + $assignCompareListToCustomer, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + $this->assertTrue($assignResponse['assignCompareListToCustomer']['result']); + + $customerAssignedResponse = $this->graphQlQuery( + $customerQuery, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('compare_list', $customerAssignedResponse['customer']); + $this->uidAssertion($customerAssignedResponse['customer']['compare_list']['uid']); + $this->itemsAssertion($customerAssignedResponse['customer']['compare_list']['items']); + } + + /** + * Assign compare list of one customer to another customer + * + * @magentoApiDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + */ + public function testCompareListsNotAccessibleBetweenCustomers() + { + $uidCustomer1 = $this->createCompareListForCustomer('customer@example.com', 'password'); + $uidcustomer2 = $this->createCompareListForCustomer('customer_two@example.com', 'password'); + $assignCompareListToCustomer = <<<MUTATION +mutation { + assignCompareListToCustomer(uid: "{$uidCustomer1}"){ + result + compare_list { + uid + items { + uid + } + } + } +} +MUTATION; + + $expectedExceptionsMessage = 'GraphQL response contains errors: This customer is not authorized to access this list'; + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage($expectedExceptionsMessage); + //customer2 not allowed to assign compareList belonging to customer1 + $this->graphQlMutation( + $assignCompareListToCustomer, + [], + '', + $this->getCustomerAuthHeaders('customer_two@example.com', 'password') + ); + + $deleteCompareList = <<<MUTATION +mutation{ + deleteCompareList(uid:"{$uidcustomer2}") { + result + } +} +MUTATION; + $expectedExceptionsMessage = 'GraphQL response contains errors: This customer is not authorized to access this list'; + $this->expectException(ResponseContainsErrorsException::class); + $this->expectExceptionMessage($expectedExceptionsMessage); + //customer1 not allowed to delete compareList belonging to customer2 + $this->graphQlMutation( + $assignCompareListToCustomer, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + + } + + /** + * Get customer Header + * + * @param string $email + * @param string $password + * + * @return array + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Create compare list + * + * @return array + */ + private function createCompareList(): array + { + $mutation = <<<MUTATION +mutation{ + createCompareList { + uid + item_count + attributes{code label} + } +} +MUTATION; + return $this->graphQlMutation($mutation); + } + + private function createCompareListForCustomer(string $username, string $password): string + { + $compareListCustomer = <<<MUTATION +mutation{ + createCompareList { + uid + } +} +MUTATION; + $response = $this->graphQlMutation( + $compareListCustomer, + [], + '', + $this->getCustomerAuthHeaders($username, $password) + ); + + return $response['createCompareList']['uid']; + } + + /** + * Add products to compare list + * + * @param $uid + * + * @return array + */ + private function addProductsToCompareList($uid): array + { + $product1 = $this->productRepository->get(self::PRODUCT_SKU_1); + $product2 = $this->productRepository->get(self::PRODUCT_SKU_2); + $addProductsToCompareList = <<<MUTATION +mutation{ + addProductsToCompareList(input: { uid: "{$uid}", products: [{$product1->getId()}, {$product2->getId()}]}) { + uid + item_count + attributes{code label} + items { + product { + sku + } + } + } +} +MUTATION; + return $this->graphQlMutation($addProductsToCompareList); + } + + /** + * Assert UID + * + * @param string $uid + */ + private function uidAssertion(string $uid) + { + $this->assertIsString($uid); + $this->assertEquals(32, strlen($uid)); + } + + /** + * Assert products + * + * @param array $items + */ + private function itemsAssertion(array $items) + { + $this->assertArrayHasKey(0, $items); + $this->assertArrayHasKey(1, $items); + $this->assertEquals(self::PRODUCT_SKU_1, $items[0]['product']['sku']); + $this->assertEquals(self::PRODUCT_SKU_2, $items[1]['product']['sku']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php index fb6b36b883e77..5a08692d5dcdd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php @@ -64,8 +64,9 @@ public function testAddConfigurableProductToCart() $parentSku = $product['sku']; $attributeId = (int) $product['configurable_options'][0]['attribute_id']; $valueIndex = $product['configurable_options'][0]['values'][1]['value_index']; - + $productRowId = (string) $product['configurable_options'][0]['product_id']; $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); $query = $this->getQuery( @@ -76,19 +77,35 @@ public function testAddConfigurableProductToCart() ); $response = $this->graphQlMutation($query); - + $expectedProductOptionsValueUid = $this->generateConfigurableSelectionUID($attributeId, $valueIndex); + $expectedProductOptionsUid = base64_encode("configurable/$productRowId/$attributeId"); $cartItem = current($response['addProductsToCart']['cart']['items']); self::assertEquals($quantity, $cartItem['quantity']); self::assertEquals($parentSku, $cartItem['product']['sku']); + self::assertEquals(base64_encode((string)$cartItem['product']['id']), $cartItem['product']['uid']); self::assertArrayHasKey('configurable_options', $cartItem); $option = current($cartItem['configurable_options']); self::assertEquals($attributeId, $option['id']); self::assertEquals($valueIndex, $option['value_id']); + self::assertEquals($expectedProductOptionsValueUid, $option['configurable_product_option_value_uid']); + self::assertEquals($expectedProductOptionsUid, $option['configurable_product_option_uid']); self::assertArrayHasKey('option_label', $option); self::assertArrayHasKey('value_label', $option); } + /** + * Generates UID configurable product + * + * @param int $attributeId + * @param int $valueIndex + * @return string + */ + private function generateConfigurableSelectionUID(int $attributeId, int $valueIndex): string + { + return base64_encode("configurable/$attributeId/$valueIndex"); + } + /** * Generates UID for super configurable product super attributes * @@ -98,7 +115,7 @@ public function testAddConfigurableProductToCart() */ private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string { - return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; + return 'selected_options: ["' . $this->generateConfigurableSelectionUID($attributeId, $valueIndex) . '"]'; } /** @@ -256,15 +273,20 @@ private function getQuery( cart { items { id + uid quantity product { sku + uid + id } ... on ConfigurableCartItem { configurable_options { id + configurable_product_option_uid option_label value_id + configurable_product_option_value_uid value_label } } @@ -306,16 +328,20 @@ private function getFetchProductQuery(string $term): string ) { items { sku + uid ... on ConfigurableProduct { configurable_options { attribute_id + attribute_uid attribute_code id + uid label position product_id use_default values { + uid default_label label store_label @@ -323,6 +349,17 @@ private function getFetchProductQuery(string $term): string value_index } } + configurable_options_selection_metadata { + options_available_for_selection { + attribute_code + option_value_uids + } + variant { + uid + name + attribute_set_id + } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php index 49bbabc212fc2..a5431efefdde3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableProductViewTest.php @@ -39,12 +39,9 @@ public function testQueryConfigurableProductLinks() products(filter: {sku: {eq: "{$productSku}"}}) { items { id - attribute_set_id - created_at name sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -115,12 +112,9 @@ public function testQueryConfigurableProductLinks() id name sku - attribute_set_id ... on PhysicalProductInterface { weight } - created_at - updated_at price { minimalPrice { amount { @@ -237,8 +231,6 @@ private function assertBaseFields($product, $actualResponse) /** @var MetadataPool $metadataPool */ $metadataPool = ObjectManager::getInstance()->get(MetadataPool::class); $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], [ 'response_field' => 'id', 'expected_value' => $product->getData( @@ -250,7 +242,6 @@ private function assertBaseFields($product, $actualResponse) ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ['response_field' => 'weight', 'expected_value' => $product->getWeight()], [ 'response_field' => 'price', diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php index f1b08d8858ba0..8be20653070e1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/RemoveConfigurableProductFromCartTest.php @@ -53,14 +53,20 @@ protected function setUp(): void } /** + * @param string $itemArgName + * @param string $reservedOrderId + * @dataProvider removeConfigurableProductFromCartDataProvider * @magentoApiDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php */ - public function testRemoveConfigurableProductFromCart() + public function testRemoveConfigurableProductFromCart(string $itemArgName, string $reservedOrderId) { $configurableOptionSku = 'simple_10'; - $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_cart_with_configurable'); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); $quoteItemId = $this->getQuoteItemIdBySku($configurableOptionSku); - $query = $this->getQuery($maskedQuoteId, $quoteItemId); + if ($itemArgName === 'cart_item_uid') { + $quoteItemId = base64_encode($quoteItemId); + } + $query = $this->getQuery($itemArgName, $maskedQuoteId, $quoteItemId); $response = $this->graphQlMutation($query); $this->assertArrayHasKey('cart', $response['removeItemFromCart']); @@ -69,18 +75,37 @@ public function testRemoveConfigurableProductFromCart() } /** + * Data provider for testUpdateConfigurableCartItemQuantity + * + * @return array + */ + public function removeConfigurableProductFromCartDataProvider(): array + { + return [ + ['cart_item_id', 'test_cart_with_configurable'], + ['cart_item_uid', 'test_cart_with_configurable'], + ]; + } + + /** + * @param string $itemArgName * @param string $maskedQuoteId - * @param int $itemId + * @param string $itemId * @return string */ - private function getQuery(string $maskedQuoteId, int $itemId): string + private function getQuery(string $itemArgName, string $maskedQuoteId, string $itemId): string { + if (is_numeric($itemId)) { + $itemId = (int) $itemId; + } else { + $itemId = '"' . $itemId . '"'; + } return <<<QUERY mutation { removeItemFromCart( input: { cart_id: "{$maskedQuoteId}" - cart_item_id: {$itemId} + {$itemArgName}: {$itemId} } ) { cart { @@ -97,9 +122,9 @@ private function getQuery(string $maskedQuoteId, int $itemId): string * Returns quote item ID by product's SKU * * @param string $sku - * @return int + * @return string */ - private function getQuoteItemIdBySku(string $sku): int + private function getQuoteItemIdBySku(string $sku): string { $quote = $this->quoteFactory->create(); $this->quoteResource->load($quote, 'test_cart_with_configurable', 'reserved_order_id'); @@ -107,7 +132,7 @@ private function getQuoteItemIdBySku(string $sku): int $quoteItemsCollection = $quote->getItemsCollection(); foreach ($quoteItemsCollection->getItems() as $item) { if ($item->getSku() == $sku) { - return (int)$item->getId(); + return $item->getId(); } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/StoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/StoreConfigTest.php new file mode 100644 index 0000000000000..0c4dca9a642d2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/StoreConfigTest.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Exception; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Class for Store Config Configurable Product Image settings + */ +class StoreConfigTest extends GraphQlAbstract +{ + /** + * Check type of configurable_thumbnail_source storeConfig configurable_thumbnail_source + * + * @magentoConfigFixture default_store checkout/cart/configurable_product_image itself + * + * @throws Exception + */ + public function testReturnTypeAutocompleteOnStorefrontConfig() + { + $query = <<<QUERY +{ + storeConfig { + configurable_thumbnail_source + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('configurable_thumbnail_source', $response['storeConfig']); + self::assertEquals('itself', $response['storeConfig']['configurable_thumbnail_source']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php index d3bc0204efe23..c7800ec327e90 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/UpdateConfigurableCartItemsTest.php @@ -44,18 +44,22 @@ class UpdateConfigurableCartItemsTest extends GraphQlAbstract private $quoteResource; /** + * @param string $itemArgName + * @param string $reservedOrderId + * @dataProvider updateConfigurableCartItemQuantityDataProvider * @magentoApiDataFixture Magento/ConfigurableProduct/_files/quote_with_configurable_product.php */ - public function testUpdateConfigurableCartItemQuantity() + public function testUpdateConfigurableCartItemQuantity(string $itemArgName, string $reservedOrderId) { - $reservedOrderId = 'test_cart_with_configurable'; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); $productSku = 'simple_10'; $newQuantity = 123; - $quoteItem = $this->getQuoteItemBySku($productSku, $reservedOrderId); - - $query = $this->getQuery($maskedQuoteId, (int)$quoteItem->getId(), $newQuantity); + $quoteItemId = $this->getQuoteItemBySku($productSku, $reservedOrderId)->getId(); + if ($itemArgName === 'cart_item_uid') { + $quoteItemId = base64_encode($quoteItemId); + } + $query = $this->getQuery($itemArgName, $maskedQuoteId, $quoteItemId, $newQuantity); $response = $this->graphQlMutation($query); self::assertArrayHasKey('updateCartItems', $response); @@ -63,6 +67,19 @@ public function testUpdateConfigurableCartItemQuantity() self::assertEquals($newQuantity, $response['updateCartItems']['cart']['items']['0']['quantity']); } + /** + * Data provider for testUpdateConfigurableCartItemQuantity + * + * @return array + */ + public function updateConfigurableCartItemQuantityDataProvider(): array + { + return [ + ['cart_item_id', 'test_cart_with_configurable'], + ['cart_item_uid', 'test_cart_with_configurable'], + ]; + } + /** * @inheritdoc */ @@ -76,20 +93,26 @@ protected function setUp(): void } /** + * @param string $itemArgName * @param string $maskedQuoteId - * @param int $quoteItemId + * @param string $quoteItemId * @param int $newQuantity * @return string */ - private function getQuery(string $maskedQuoteId, int $quoteItemId, int $newQuantity): string + private function getQuery(string $itemArgName, string $maskedQuoteId, string $quoteItemId, int $newQuantity): string { + if (is_numeric($quoteItemId)) { + $quoteItemId = (int) $quoteItemId; + } else { + $quoteItemId = '"' . $quoteItemId . '"'; + } return <<<QUERY mutation { updateCartItems(input: { cart_id:"$maskedQuoteId" cart_items: [ { - cart_item_id: $quoteItemId + $itemArgName: $quoteItemId quantity: $newQuantity } ] diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/DownloadableProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/DownloadableProductViewTest.php index c2a1b0778d3c6..774e4dcbdf297 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/DownloadableProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/DownloadableProductViewTest.php @@ -33,12 +33,9 @@ public function testQueryAllFieldsDownloadableProductsWithDownloadableFileAndSam { items{ id - attribute_set_id - created_at name sku type_id - updated_at price{ regularPrice{ amount{ @@ -110,12 +107,9 @@ public function testDownloadableProductQueryWithNoSample() { items{ id - attribute_set_id - created_at name sku type_id - updated_at ...on PhysicalProductInterface{ weight } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php index 2ab7f50b86ae9..a1e2e90c3f0a1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/QueryComplexityLimiterTest.php @@ -46,10 +46,7 @@ public function testQueryComplexityIsLimited() file } name - special_from_date special_to_date - new_to_date - new_from_date tier_price manufacturer thumbnail { @@ -62,8 +59,6 @@ public function testQueryComplexityIsLimited() label } canonical_url - updated_at - created_at categories { id position @@ -79,14 +74,11 @@ public function testQueryComplexityIsLimited() products { items { name - special_from_date special_to_date - new_to_date thumbnail { url label } - new_from_date tier_price manufacturer sku @@ -95,8 +87,6 @@ public function testQueryComplexityIsLimited() label } canonical_url - updated_at - created_at media_gallery_entries { position id @@ -117,10 +107,7 @@ public function testQueryComplexityIsLimited() products { items { name - special_from_date special_to_date - new_to_date - new_from_date tier_price manufacturer thumbnail { @@ -133,8 +120,6 @@ public function testQueryComplexityIsLimited() label } canonical_url - updated_at - created_at categories { id position @@ -150,10 +135,7 @@ public function testQueryComplexityIsLimited() products { items { name - special_from_date special_to_date - new_to_date - new_from_date tier_price manufacturer sku @@ -162,8 +144,6 @@ public function testQueryComplexityIsLimited() label } canonical_url - updated_at - created_at categories { id position @@ -179,7 +159,6 @@ public function testQueryComplexityIsLimited() products { items { name - special_from_date special_to_date price { minimalPrice { @@ -323,8 +302,24 @@ public function testQueryComplexityIsLimited() percentage_value website_id } - new_to_date - new_from_date + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } + tier_prices { + customer_group_id + qty + percentage_value + website_id + } tier_price manufacturer sku @@ -337,8 +332,6 @@ public function testQueryComplexityIsLimited() label } canonical_url - updated_at - created_at categories { id position @@ -360,6 +353,16 @@ public function testQueryComplexityIsLimited() position position position + position + position + position + position + position + position + position + position + position + position level url_key url_path diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/RequiredInputArgumentTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/RequiredInputArgumentTest.php index 9fecc954d1182..f2189ad020e3f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/RequiredInputArgumentTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Framework/RequiredInputArgumentTest.php @@ -26,15 +26,16 @@ public function testSimpleInputArgumentRequired() { $query = <<<QUERY { - urlResolver{ - id - type + testQueryWithTopLevelMandatoryInputArguments{ + item_id + name } } QUERY; $expectedExceptionsMessage = 'GraphQL response contains errors:' - . ' Field "urlResolver" argument "url" of type "String!" is required but not provided.'; + . ' Field "testQueryWithTopLevelMandatoryInputArguments" argument "topLevelArgument"' + . ' of type "String!" is required but not provided.'; $this->expectException(ResponseContainsErrorsException::class); $this->expectExceptionMessage($expectedExceptionsMessage); @@ -44,31 +45,30 @@ public function testSimpleInputArgumentRequired() /** * Test that a more complex required argument is handled properly * - * updateCartItems mutation has required parameter input.cart_items.cart_item_id + * testInputQueryWithMandatoryArguments mutation has required parameter input.query_items.query_item_id */ public function testInputObjectArgumentRequired() { $query = <<<QUERY - mutation { - updateCartItems( + query { + testQueryWithNestedMandatoryInputArguments( input: { - cart_id: "foobar" - cart_items: [ + query_id: "foobar" + query_items: [ { quantity: 2 } ] } ) { - cart { - total_quantity - } + item_id + name } } QUERY; $expectedExceptionsMessage = 'GraphQL response contains errors:' - . ' Field CartItemUpdateInput.cart_item_id of required type Int! was not provided.'; + . ' Field QueryWithMandatoryArgumentsInput.query_item_id of required type Int! was not provided.'; $this->expectException(ResponseContainsErrorsException::class); $this->expectExceptionMessage($expectedExceptionsMessage); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/AddGroupedProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/AddGroupedProductToWishlistTest.php index a9c9e0e104235..7a5fdb0a242fa 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/AddGroupedProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/AddGroupedProductToWishlistTest.php @@ -59,9 +59,9 @@ public function testAllFieldsGroupedProduct() $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); - $this->assertEquals((int) $item->getQty(), $response['items_v2'][0]['quantity']); - $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); - $this->assertEquals($productSku, $response['items_v2'][0]['product']['sku']); + $this->assertEquals((int) $item->getQty(), $response['items_v2']['items'][0]['quantity']); + $this->assertEquals($item->getAddedAt(), $response['items_v2']['items'][0]['added_at']); + $this->assertEquals($productSku, $response['items_v2']['items'][0]['product']['sku']); } private function getMutation( @@ -90,13 +90,16 @@ private function getMutation( items_count updated_at items_v2 { - id + items{ + id description quantity added_at product { sku } + } + } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php index 8cb0a6db972b4..ac893aa10fd55 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php @@ -42,8 +42,6 @@ public function testAllFieldsGroupedProduct() products(filter: {sku: {eq: "{$productSku}"}}) { items { id - attribute_set_id - created_at name sku type_id diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php index 9eea2396c24ce..86ba62f8e41ec 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/ProductViewTest.php @@ -106,13 +106,11 @@ private function getQuery(string $sku): string return <<<QUERY { products(filter: {sku: {eq: "{$sku}"}}) { - items { + items { id - attribute_set_id - created_at name sku - type_id + type_id ... on GroupedProduct { items{ qty @@ -121,7 +119,7 @@ private function getQuery(string $sku): string sku name type_id - url_key + url_key } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/CreateCustomerV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/CreateCustomerV2Test.php new file mode 100644 index 0000000000000..57d5bbff1786d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/CreateCustomerV2Test.php @@ -0,0 +1,188 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\LoginAsCustomerGraphQl; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for create customer (V2) with allow_remote_shopping_assistance input/output + */ +class CreateCustomerV2Test extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + protected function setUp(): void + { + parent::setUp(); + + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + } + + /** + * Test setting allow_remote_shopping_assistance to true + * + * @throws \Exception + */ + public function testCreateCustomerAccountWithAllowTrue() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + allow_remote_shopping_assistance: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + allow_remote_shopping_assistance + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertNull($response['createCustomerV2']['customer']['id']); + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + $this->assertTrue($response['createCustomerV2']['customer']['allow_remote_shopping_assistance']); + } + + /** + * Test setting allow_remote_shopping_assistance to false + * + * @throws \Exception + */ + public function testCreateCustomerAccountWithAllowFalse() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + allow_remote_shopping_assistance: false + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + allow_remote_shopping_assistance + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertNull($response['createCustomerV2']['customer']['id']); + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + $this->assertFalse($response['createCustomerV2']['customer']['allow_remote_shopping_assistance']); + } + + /** + * Test omitting allow_remote_shopping_assistance + * + * @throws \Exception + */ + public function testCreateCustomerAccountWithoutAllow() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true, + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + allow_remote_shopping_assistance + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertNull($response['createCustomerV2']['customer']['id']); + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + $this->assertFalse($response['createCustomerV2']['customer']['allow_remote_shopping_assistance']); + } + + protected function tearDown(): void + { + $newEmail = 'new_customer@example.com'; + try { + $customer = $this->customerRepository->get($newEmail); + } catch (\Exception $exception) { + return; + } + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($customer); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/GenerateLoginCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/GenerateLoginCustomerTokenTest.php new file mode 100755 index 0000000000000..6d7731836656e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/GenerateLoginCustomerTokenTest.php @@ -0,0 +1,227 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\LoginAsCustomerGraphQl; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\AdminTokenServiceInterface as AdminTokenService; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * API-functional tests cases for generateCustomerToken mutation + * @SuppressWarnings(PHPMD) + */ +class GenerateLoginCustomerTokenTest extends GraphQlAbstract +{ + + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var AdminTokenService + */ + private $adminTokenService; + + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->adminTokenService = $objectManager->get(AdminTokenService::class); + } + + /** + * Verify with Admin email ID and Magento_LoginAsCustomer::login is enabled + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/admin.php + * @magentoConfigFixture admin_store login_as_customer/general/enabled 1 + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php + * @throws Exception + */ + public function testGenerateCustomerValidTokenLoginAsCustomerEnabled() + { + $customerEmail = 'customer@example.com'; + + $mutation = $this->getQuery($customerEmail); + + $response = $this->graphQlMutation( + $mutation, + [], + '', + $this->getAdminHeaderAuthentication('TestAdmin1', \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD) + ); + $this->assertArrayHasKey('generateCustomerTokenAsAdmin', $response); + $this->assertIsArray($response['generateCustomerTokenAsAdmin']); + } + + /** + * Verify with Admin email ID and Magento_LoginAsCustomer::login is disabled + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/admin.php + * @magentoConfigFixture admin_store login_as_customer/general/enabled 0 + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php + * @throws Exception + */ + public function testGenerateCustomerValidTokenLoginAsCustomerDisabled() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Login as Customer is disabled."); + + $customerEmail = 'customer@example.com'; + + $mutation = $this->getQuery($customerEmail); + $response = $this->graphQlMutation( + $mutation, + [], + '', + $this->getAdminHeaderAuthentication('TestAdmin1', \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD) + ); + } + + /** + * Verify with Customer Token in auth header + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php + * @magentoConfigFixture admin_store login_as_customer/general/enabled 1 + * @throws Exception + */ + public function testGenerateCustomerTokenLoginWithCustomerCredentials() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("The current customer isn't authorized."); + + $customerEmail = 'customer@example.com'; + $password = 'password'; + + $mutation = $this->getQuery($customerEmail); + + $this->graphQlMutation( + $mutation, + [], + '', + $this->getCustomerHeaderAuthentication($customerEmail, $password) + ); + } + + /** + * Test with invalid data. + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/admin.php + * @magentoConfigFixture admin_store login_as_customer/general/enabled 1 + * + * @dataProvider dataProviderInvalidInfo + * @param string $adminUserName + * @param string $adminPassword + * @param string $customerEmail + * @param string $message + */ + public function testGenerateCustomerTokenInvalidData( + string $adminUserName, + string $adminPassword, + string $customerEmail, + string $message + ) { + $this->expectException(Exception::class); + $this->expectExceptionMessage($message); + + $mutation = $this->getQuery($customerEmail); + $this->graphQlMutation( + $mutation, + [], + '', + $this->getAdminHeaderAuthentication($adminUserName, $adminPassword) + ); + } + + /** + * Provides invalid test cases data + * + * @return array + */ + public function dataProviderInvalidInfo(): array + { + return [ + 'invalid_admin_user_name' => [ + 'TestAdmin(^%', + \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, + 'customer@example.com', + 'The account sign-in was incorrect or your account is disabled temporarily. ' . + 'Please wait and try again later.' + ], + 'invalid_admin_password' => [ + 'TestAdmin1', + 'invalid_password', + 'customer@example.com', + 'The account sign-in was incorrect or your account is disabled temporarily. ' . + 'Please wait and try again later.' + ] + ]; + } + + /** + * @param string $customerEmail + * @return string + */ + private function getQuery(string $customerEmail) : string + { + return <<<MUTATION +mutation{ + generateCustomerTokenAsAdmin(input: { + customer_email: "{$customerEmail}" + }){ + customer_token + } +} +MUTATION; + } + + /** + * Generate customer authentication token + * + * @magentoApiDataFixture Magento/LoginAsCustomer/_files/admin.php + * + * @param string $username + * @param string $password + * @return string[] + * @throws AuthenticationException + */ + public function getCustomerHeaderAuthentication( + string $username = 'github@gmail.com', + string $password = \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD + ): array { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * To get admin access token + * + * @param string $userName + * @param string $password + * @return string[] + * @throws AuthenticationException + */ + private function getAdminHeaderAuthentication(string $userName, string $password) + { + try { + $adminAccessToken = $this->adminTokenService->createAdminAccessToken($userName, $password); + return ['Authorization' => 'Bearer ' . $adminAccessToken]; + } catch (\Exception $e) { + throw new AuthenticationException( + __( + 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.' + ) + ); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/UpdateCustomerV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/UpdateCustomerV2Test.php new file mode 100644 index 0000000000000..f47f7c1d1c3db --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/LoginAsCustomerGraphQl/UpdateCustomerV2Test.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\LoginAsCustomerGraphQl; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for update customer (V2) with allow_remote_shopping_assistance input/output + */ +class UpdateCustomerV2Test extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + protected function setUp(): void + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomer(): void + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + allow_remote_shopping_assistance: true + } + ) { + customer { + allow_remote_shopping_assistance + } + } +} +QUERY; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertTrue($response['updateCustomerV2']['customer']['allow_remote_shopping_assistance']); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } + +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php index 20a612e9f88b0..40280f5c7b2c7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/PageCache/ProductInMultipleStoresCacheTest.php @@ -93,8 +93,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNonExisti products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -107,7 +105,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNonExisti } sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -138,8 +135,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNotAllowe products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -152,7 +147,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrencyNotAllowe } sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -187,8 +181,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrency() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -201,7 +193,6 @@ public function testProductFromSpecificAndDefaultStoreWithMultiCurrency() } sku type_id - updated_at ... on PhysicalProductInterface { weight } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php index 4e50f6ff3a2ca..418258478d4d7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php @@ -81,10 +81,31 @@ public function testAddSimpleProductWithOptions() $customizableOptionsOutput = $response['addProductsToCart']['cart']['items'][0]['customizable_options']; - foreach ($customizableOptionsOutput as $customizableOptionOutput) { + foreach ($customizableOptionsOutput as $key => $customizableOptionOutput) { $customizableOptionOutputValues = []; foreach ($customizableOptionOutput['values'] as $customizableOptionOutputValue) { $customizableOptionOutputValues[] = $customizableOptionOutputValue['value']; + + $decodedOptionValue = base64_decode($customizableOptionOutputValue['customizable_option_value_uid']); + $decodedArray = explode('/', $decodedOptionValue); + if (count($decodedArray) === 2) { + self::assertEquals( + base64_encode('custom-option/' . $customizableOptionOutput['id']), + $customizableOptionOutputValue['customizable_option_value_uid'] + ); + } elseif (count($decodedArray) === 3) { + self::assertEquals( + base64_encode( + 'custom-option/' + . $customizableOptionOutput['id'] + . '/' + . $customizableOptionOutputValue['value'] + ), + $customizableOptionOutputValue['customizable_option_value_uid'] + ); + } else { + self::fail('customizable_option_value_uid '); + } } if (count($customizableOptionOutputValues) === 1) { $customizableOptionOutputValues = $customizableOptionOutputValues[0]; @@ -94,6 +115,11 @@ public function testAddSimpleProductWithOptions() $decodedItemOptions[$customizableOptionOutput['id']], $customizableOptionOutputValues ); + + self::assertEquals( + base64_encode((string) 'custom-option/' . $customizableOptionOutput['id']), + $customizableOptionOutput['customizable_option_uid'] + ); } } @@ -242,8 +268,11 @@ private function getAddToCartMutation( customizable_options { label id + customizable_option_uid values { value + customizable_option_value_uid + id } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php index d51e632035dfc..f31b396a18ba6 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/EditQuoteItemWithCustomOptionsTest.php @@ -188,13 +188,14 @@ public function testOptionSetPersistsOnExtraOptionWithIncorrectId() */ private function getQuery(string $maskedQuoteId, int $quoteItemId, $customizableOptionsQuery): string { + $base64EncodedItemId = base64_encode((string) $quoteItemId); return <<<QUERY mutation { updateCartItems(input: { cart_id:"$maskedQuoteId" cart_items: [ { - cart_item_id: $quoteItemId + cart_item_uid: "$base64EncodedItemId" quantity: 1 customizable_options: $customizableOptionsQuery } @@ -209,9 +210,11 @@ private function getQuery(string $maskedQuoteId, int $quoteItemId, $customizable ... on SimpleCartItem { customizable_options { label + customizable_option_uid values { label value + customizable_option_value_uid } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php index 1f6c8aca54ea5..1468730028b6a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddSimpleProductToCartTest.php @@ -81,6 +81,56 @@ public function testAddSimpleProductToCart() self::assertEquals('USD', $rowTotalIncludingTax['currency']); } + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + */ + public function testAddSimpleProductWithDifferentStoreHeader() + { + $sku = 'simple_product'; + $quantity = 2; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $headerMap = ['Store' => 'fixture_second_store']; + $query = $this->getQuery($maskedQuoteId, $sku, $quantity); + $response = $this->graphQlMutation($query, [], '', $headerMap); + self::assertArrayHasKey('cart', $response['addSimpleProductsToCart']); + + self::assertArrayHasKey('shipping_addresses', $response['addSimpleProductsToCart']['cart']); + self::assertEmpty($response['addSimpleProductsToCart']['cart']['shipping_addresses']); + self::assertEquals($quantity, $response['addSimpleProductsToCart']['cart']['items'][0]['quantity']); + self::assertEquals($sku, $response['addSimpleProductsToCart']['cart']['items'][0]['product']['sku']); + self::assertArrayHasKey('prices', $response['addSimpleProductsToCart']['cart']['items'][0]); + self::assertArrayHasKey('id', $response['addSimpleProductsToCart']['cart']); + self::assertEquals($maskedQuoteId, $response['addSimpleProductsToCart']['cart']['id']); + + self::assertArrayHasKey('price', $response['addSimpleProductsToCart']['cart']['items'][0]['prices']); + $price = $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['price']; + self::assertArrayHasKey('value', $price); + self::assertEquals(10, $price['value']); + self::assertArrayHasKey('currency', $price); + self::assertEquals('USD', $price['currency']); + + self::assertArrayHasKey('row_total', $response['addSimpleProductsToCart']['cart']['items'][0]['prices']); + $rowTotal = $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['row_total']; + self::assertArrayHasKey('value', $rowTotal); + self::assertEquals(20, $rowTotal['value']); + self::assertArrayHasKey('currency', $rowTotal); + self::assertEquals('USD', $rowTotal['currency']); + + self::assertArrayHasKey( + 'row_total_including_tax', + $response['addSimpleProductsToCart']['cart']['items'][0]['prices'] + ); + $rowTotalIncludingTax = + $response['addSimpleProductsToCart']['cart']['items'][0]['prices']['row_total_including_tax']; + self::assertArrayHasKey('value', $rowTotalIncludingTax); + self::assertEquals(20, $rowTotalIncludingTax['value']); + self::assertArrayHasKey('currency', $rowTotalIncludingTax); + self::assertEquals('USD', $rowTotalIncludingTax['currency']); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/product_with_image_no_options.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php index 858c38cc72dfd..b2c6902f5aa97 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetCartTest.php @@ -8,7 +8,12 @@ namespace Magento\GraphQl\Quote\Guest; use Exception; +use Magento\Config\App\Config\Type\System; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Directory\Model\Currency; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -91,7 +96,9 @@ public function testGetCartIfCartIdIsEmpty() public function testGetCartIfCartIdIsMissed() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Field "cart" argument "cart_id" of type "String!" is required but not provided.'); + $this->expectExceptionMessage( + 'Field "cart" argument "cart_id" of type "String!" is required but not provided.' + ); $query = <<<QUERY { @@ -151,12 +158,79 @@ public function testGetCartWithNotDefaultStore() /** * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php * @magentoApiDataFixture Magento/Store/_files/second_store.php - * */ public function testGetCartWithWrongStore() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Store/_files/second_store_with_second_currency.php + */ + public function testGetCartWithDifferentStoreDifferentCurrency() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute( + 'test_order_with_simple_product_without_address' + ); + $query = $this->getQuery($maskedQuoteId); + + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + self::assertArrayHasKey('prices', $response['cart']['items'][0]); + $price = $response['cart']['items'][0]['prices']['price']; + self::assertEquals(20, $price['value']); + self::assertEquals('EUR', $price['currency']); + + // test alternate currency in header + $objectManager = Bootstrap::getObjectManager(); + $store = $objectManager->create(Store::class); + $store->load('fixture_second_store', 'code'); + if ($storeId = $store->load('fixture_second_store', 'code')->getId()) { + /** @var \Magento\Config\Model\ResourceModel\Config $configResource */ + $configResource = $objectManager->get(Config::class); + $configResource->saveConfig( + Currency::XML_PATH_CURRENCY_ALLOW, + 'USD', + ScopeInterface::SCOPE_STORES, + $storeId + ); + /** + * Configuration cache clean is required to reload currency setting + */ + /** @var System $config */ + $config = $objectManager->get(System::class); + $config->clean(); + } + $headerMap['Content-Currency'] = 'USD'; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + self::assertArrayHasKey('cart', $response); + self::assertArrayHasKey('items', $response['cart']); + self::assertArrayHasKey('prices', $response['cart']['items'][0]); + $price = $response['cart']['items'][0]['prices']['price']; + self::assertEquals(10, $price['value']); + self::assertEquals('USD', $price['currency']); + } + + /** + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + */ + public function testGetCartWithDifferentStoreDifferentWebsite() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Wrong store code specified for cart'); + $this->expectExceptionMessage('Can\'t assign cart to store in different website.'); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); $query = $this->getQuery($maskedQuoteId); @@ -199,6 +273,12 @@ private function getQuery(string $maskedQuoteId): string product { sku } + prices { + price { + value + currency + } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php index 72d35fdd51b96..db9a12e654a2c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -327,6 +327,45 @@ public function testPlaceOrderOfCustomerCart() $this->graphQlMutation($query); } + /** + * Test place order with gift message options + * + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoConfigFixture default_store carriers/flatrate/active 1 + * @magentoConfigFixture default_store carriers/tablerate/active 1 + * @magentoConfigFixture default_store carriers/freeshipping/active 1 + * @magentoConfigFixture default_store payment/banktransfer/active 1 + * @magentoConfigFixture default_store payment/cashondelivery/active 1 + * @magentoConfigFixture default_store payment/checkmo/active 1 + * @magentoConfigFixture default_store payment/purchaseorder/active 1 + * @magentoConfigFixture sales/gift_options/allow_order 1 + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 1 + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_gift_options.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrderWithGiftMessage() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); + $orderIncrementId = $response['placeOrder']['order']['order_number']; + $order = $this->orderFactory->create(); + $order->loadByIncrementId($orderIncrementId); + $this->assertNotEmpty($order->getGiftMessageId()); + } + /** * @param string $maskedQuoteId * @return string diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php index 7a14aca72d83f..e3c77a40a470a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetGuestEmailOnCartTest.php @@ -8,6 +8,9 @@ namespace Magento\GraphQl\Quote\Guest; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\Quote\Model\Quote\Address; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -21,10 +24,22 @@ class SetGuestEmailOnCartTest extends GraphQlAbstract */ private $getMaskedQuoteIdByReservedOrderId; + /** + * @var QuoteFactory + */ + private $quoteFactory; + + /** + * @var QuoteResource + */ + private $quoteResource; + protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->quoteFactory = $objectManager->get(QuoteFactory::class); + $this->quoteResource = $objectManager->get(QuoteResource::class); } /** @@ -43,6 +58,33 @@ public function testSetGuestEmailOnCart() $this->assertEquals($email, $response['setGuestEmailOnCart']['cart']['email']); } + /** + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + */ + public function testSetGuestEmailOnCartWithDifferentEmailAddress() + { + $reservedOrderId = 'test_quote'; + $secondEmail = 'attempt2@example.com'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $email = 'attempt1@example.com'; + $query = $this->getQuery($maskedQuoteId, $email); + $this->graphQlMutation($query); + + $query = $this->getQuery($maskedQuoteId, $secondEmail); + $this->graphQlMutation($query); + + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, $reservedOrderId, 'reserved_order_id'); + $addresses = $quote->getAddressesCollection(); + $this->assertEquals(2, $addresses->count()); + foreach ($addresses as $address) { + if ($address->getAddressType() === Address::ADDRESS_TYPE_SHIPPING) { + $this->assertEquals($secondEmail, $address->getEmail()); + } + } + } + /** * _security * @magentoApiDataFixture Magento/Customer/_files/customer.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php index cb210b180682c..f2cf90c95de18 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php @@ -25,13 +25,12 @@ public function testQueryRelatedProducts() { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { related_products { sku name url_key - created_at } } } @@ -60,13 +59,12 @@ public function testQueryDisableRelatedProduct() { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { related_products { sku name url_key - created_at } } } @@ -94,13 +92,12 @@ public function testQueryCrossSellProducts() { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { crosssell_products { sku name url_key - created_at } } } @@ -119,11 +116,9 @@ public function testQueryCrossSellProducts() self::assertArrayHasKey('sku', $crossSellProduct); self::assertArrayHasKey('name', $crossSellProduct); self::assertArrayHasKey('url_key', $crossSellProduct); - self::assertArrayHasKey('created_at', $crossSellProduct); self::assertEquals($crossSellProduct['sku'], 'simple'); self::assertEquals($crossSellProduct['name'], 'Simple Cross Sell'); self::assertEquals($crossSellProduct['url_key'], 'simple-cross-sell'); - self::assertNotEmpty($crossSellProduct['created_at']); } /** @@ -137,13 +132,12 @@ public function testQueryUpSellProducts() { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { upsell_products { sku name url_key - created_at } } } @@ -162,11 +156,9 @@ public function testQueryUpSellProducts() self::assertArrayHasKey('sku', $upSellProduct); self::assertArrayHasKey('name', $upSellProduct); self::assertArrayHasKey('url_key', $upSellProduct); - self::assertArrayHasKey('created_at', $upSellProduct); self::assertEquals($upSellProduct['sku'], 'simple'); self::assertEquals($upSellProduct['name'], 'Simple Up Sell'); self::assertEquals($upSellProduct['url_key'], 'simple-up-sell'); - self::assertNotEmpty($upSellProduct['created_at']); } /** @@ -190,14 +182,12 @@ private function assertRelatedProducts(array $relatedProducts): void self::assertArrayHasKey('sku', $product); self::assertArrayHasKey('name', $product); self::assertArrayHasKey('url_key', $product); - self::assertArrayHasKey('created_at', $product); self::assertArrayHasKey($product['sku'], $expectedData); $productExpectedData = $expectedData[$product['sku']]; self::assertEquals($product['name'], $productExpectedData['name']); self::assertEquals($product['url_key'], $productExpectedData['url_key']); - self::assertNotEmpty($product['created_at']); } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php index b2d25c7418866..e2897d6e16bfe 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Tax/ProductViewTest.php @@ -144,8 +144,6 @@ public function testQueryAllFieldsSimpleProduct() products(filter: {sku: {eq: "{$productSku}"}}) { items { - attribute_set_id - created_at id name price { @@ -194,7 +192,6 @@ public function testQueryAllFieldsSimpleProduct() } sku type_id - updated_at ... on PhysicalProductInterface { weight } @@ -281,8 +278,6 @@ private function assertBaseFields($product, $actualResponse) } // product_object_field_name, expected_value $assertionMap = [ - ['response_field' => 'attribute_set_id', 'expected_value' => $product->getAttributeSetId()], - ['response_field' => 'created_at', 'expected_value' => $product->getCreatedAt()], ['response_field' => 'id', 'expected_value' => $product->getId()], ['response_field' => 'name', 'expected_value' => $product->getName()], ['response_field' => 'price', 'expected_value' => @@ -345,7 +340,6 @@ private function assertBaseFields($product, $actualResponse) ], ['response_field' => 'sku', 'expected_value' => $product->getSku()], ['response_field' => 'type_id', 'expected_value' => $product->getTypeId()], - ['response_field' => 'updated_at', 'expected_value' => $product->getUpdatedAt()], ['response_field' => 'weight', 'expected_value' => $product->getWeight()], ]; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php index ab3fed044ea97..594863f319b2b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlMutationTest.php @@ -21,8 +21,8 @@ public function testMutation() $query = <<<MUTATION mutation { testItem(id: {$id}) { - item_id, - name, + item_id + name integer_list } } @@ -47,8 +47,8 @@ public function testMutationIsNotAllowedViaGetRequest() $query = <<<MUTATION mutation { testItem(id: {$id}) { - item_id, - name, + item_id + name integer_list } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php index 12aa4444ef728..7aed048f1c4ce 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/UrlRewrite/UrlResolverTest.php @@ -37,6 +37,7 @@ public function testNonExistentEntityUrlRewrite() urlResolver(url:"{$urlPath}") { id + entity_uid relative_url type redirectCode diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php index 04518fad47052..b2de595906f5b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php @@ -85,17 +85,29 @@ public function testAddBundleProductWithOptions(): void $this->assertArrayHasKey('addProductsToWishlist', $response); $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertEmpty($response['addProductsToWishlist']['user_errors']); $response = $response['addProductsToWishlist']['wishlist']; $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); - $this->assertEquals($item->getData('qty'), $response['items_v2'][0]['quantity']); - $this->assertEquals($item->getDescription(), $response['items_v2'][0]['description']); - $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); - $this->assertNotEmpty($response['items_v2'][0]['bundle_options']); - $bundleOptions = $response['items_v2'][0]['bundle_options']; + $this->assertEquals($item->getData('qty'), $response['items_v2']['items'][0]['quantity']); + $this->assertEquals($item->getDescription(), $response['items_v2']['items'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items_v2']['items'][0]['added_at']); + $this->assertNotEmpty($response['items_v2']['items'][0]['bundle_options']); + $bundleOptions = $response['items_v2']['items'][0]['bundle_options']; $this->assertEquals('Bundle Product Items', $bundleOptions[0]['label']); $this->assertEquals(Select::NAME, $bundleOptions[0]['type']); + $bundleOptionValuesResponse = $bundleOptions[0]['values'][0]; + $this->assertNotNull($bundleOptionValuesResponse['id']); + unset($bundleOptionValuesResponse['id']); + $this->assertResponseFields( + $bundleOptionValuesResponse, + [ + 'label' => 'Simple Product', + 'quantity' => 1, + 'price' => 2.75 + ] + ); } /** @@ -121,21 +133,22 @@ public function testAddingBundleItemWithCustomOptionQuantity() $this->assertArrayHasKey('addProductsToWishlist', $response); $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertEmpty($response['addProductsToWishlist']['user_errors']); $response = $response['addProductsToWishlist']['wishlist']; $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); - $this->assertEquals($item->getData('qty'), $response['items_v2'][0]['quantity']); - $this->assertEquals($item->getDescription(), $response['items_v2'][0]['description']); - $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); - $this->assertNotEmpty($response['items_v2'][0]['bundle_options']); - $bundleOptions = $response['items_v2'][0]['bundle_options']; + $this->assertEquals($item->getData('qty'), $response['items_v2']['items'][0]['quantity']); + $this->assertEquals($item->getDescription(), $response['items_v2']['items'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items_v2']['items'][0]['added_at']); + $this->assertNotEmpty($response['items_v2']['items'][0]['bundle_options']); + $bundleOptions = $response['items_v2']['items'][0]['bundle_options']; $this->assertEquals('Option 1', $bundleOptions[0]['label']); - $bundleOptionOneValues = $bundleOptions[0]['values']; - $this->assertEquals(7, $bundleOptionOneValues[0]['quantity']); + $bundleOptionFirstValue = $bundleOptions[0]['values']; + $this->assertEquals(7, $bundleOptionFirstValue[0]['quantity']); $this->assertEquals('Option 2', $bundleOptions[1]['label']); - $bundleOptionTwoValues = $bundleOptions[1]['values']; - $this->assertEquals(1, $bundleOptionTwoValues[0]['quantity']); + $bundleOptionSecondValue = $bundleOptions[1]['values']; + $this->assertEquals(1, $bundleOptionSecondValue[0]['quantity']); } /** @@ -195,7 +208,8 @@ private function getQuery( items_count updated_at items_v2 { - id + items { + id description quantity added_at @@ -212,6 +226,8 @@ private function getQuery( } } } + } + } } } @@ -267,7 +283,8 @@ private function getQueryWithCustomOptionQuantity( items_count updated_at items_v2 { - id + items { + id description quantity added_at @@ -284,6 +301,8 @@ private function getQueryWithCustomOptionQuantity( } } } + } + } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php index cffc5eb6f93c1..25933e341564e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php @@ -68,17 +68,19 @@ public function testAddConfigurableProductWithOptions(): void $this->assertArrayHasKey('addProductsToWishlist', $response); $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertEmpty($response['addProductsToWishlist']['user_errors']); $wishlistResponse = $response['addProductsToWishlist']['wishlist']; $this->assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); $this->assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); - $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2'][0]['id']); - $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2'][0]['quantity']); - $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2'][0]['description']); - $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2'][0]['added_at']); - $this->assertNotEmpty($wishlistResponse['items_v2'][0]['configurable_options']); - $configurableOptions = $wishlistResponse['items_v2'][0]['configurable_options']; + $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2']['items'][0]['id']); + $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2']['items'][0]['quantity']); + $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2']['items'][0]['description']); + $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2']['items'][0]['added_at']); + $this->assertNotEmpty($wishlistResponse['items_v2']['items'][0]['configurable_options']); + $configurableOptions = $wishlistResponse['items_v2']['items'][0]['configurable_options']; $this->assertEquals('Test Configurable', $configurableOptions[0]['option_label']); + $this->assertEquals('Option 1', $configurableOptions[0]['value_label']); } /** @@ -138,8 +140,9 @@ private function getQuery( sharing_code items_count updated_at - items_v2 { - id + items_v2(currentPage:1,pageSize:1) { + items{ + id description quantity added_at @@ -153,6 +156,7 @@ private function getQuery( } } } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php index 0de45fb21b20b..901d1b2ee87cc 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -104,19 +104,20 @@ public function testAddDownloadableProductWithOptions(): void $this->assertArrayHasKey('addProductsToWishlist', $response); $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertEmpty($response['addProductsToWishlist']['user_errors']); $wishlistResponse = $response['addProductsToWishlist']['wishlist']; $this->assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); $this->assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); - $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2'][0]['id']); - $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2'][0]['quantity']); - $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2'][0]['description']); - $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2'][0]['added_at']); - $this->assertNotEmpty($wishlistResponse['items_v2'][0]['links_v2']); - $wishlistItemLinks = $wishlistResponse['items_v2'][0]['links_v2']; + $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2']['items'][0]['id']); + $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2']['items'][0]['quantity']); + $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2']['items'][0]['description']); + $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2']['items'][0]['added_at']); + $this->assertNotEmpty($wishlistResponse['items_v2']['items'][0]['links_v2']); + $wishlistItemLinks = $wishlistResponse['items_v2']['items'][0]['links_v2']; $this->assertEquals('Downloadable Product Link 1', $wishlistItemLinks[0]['title']); - $this->assertNotEmpty($wishlistResponse['items_v2'][0]['samples']); - $wishlistItemSamples = $wishlistResponse['items_v2'][0]['samples']; + $this->assertNotEmpty($wishlistResponse['items_v2']['items'][0]['samples']); + $wishlistItemSamples = $wishlistResponse['items_v2']['items'][0]['samples']; $this->assertEquals('Downloadable Product Sample', $wishlistItemSamples[0]['title']); } @@ -196,8 +197,10 @@ private function getQuery( sharing_code items_count updated_at - items_v2 { - id + items_v2(currentPage:1 pageSize:1) { + items + { + id description quantity added_at @@ -213,6 +216,7 @@ private function getQuery( sample_url } } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php index e452e70c24148..6ce4388877825 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php @@ -64,7 +64,7 @@ public function testCustomerWishlist(): void $this->assertEquals($wishlistItem->getItemsCount(), $wishlist['items_count']); $this->assertEquals($wishlistItem->getSharingCode(), $wishlist['sharing_code']); $this->assertEquals($wishlistItem->getUpdatedAt(), $wishlist['updated_at']); - $wishlistItemResponse = $wishlist['items_v2'][0]; + $wishlistItemResponse = $wishlist['items_v2']['items'][0]; $this->assertEquals('simple', $wishlistItemResponse['product']['sku']); } @@ -113,8 +113,7 @@ private function getQuery(): string sharing_code updated_at items_v2 { - product { - sku + items {product {name sku} } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php index dd7a54cff32a0..344b4e0f93d0d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -40,19 +40,21 @@ protected function setUp(): void public function testDeleteWishlistItemFromWishlist(): void { $wishlist = $this->getWishlist(); - $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlist = $wishlist['customer']['wishlist']; - $wishlistItems = $wishlist['items_v2']; - $this->assertEquals(1, $wishlist['items_count']); + $customerWishlists = $wishlist['customer']['wishlists'][0]; + $wishlistId = $customerWishlists['id']; - $query = $this->getQuery((int) $wishlistId, (int) $wishlistItems[0]['id']); + $wishlistItems = $customerWishlists['items_v2']['items']; + $this->assertEquals(1, $customerWishlists['items_count']); + + $query = $this->getQuery($wishlistId, $wishlistItems[0]['id']); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); $this->assertArrayHasKey('removeProductsFromWishlist', $response); $this->assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']); + $this->assertEmpty($response['removeProductsFromWishlist']['user_errors'], 'User error is not empty'); $wishlistResponse = $response['removeProductsFromWishlist']['wishlist']; $this->assertEquals(0, $wishlistResponse['items_count']); - $this->assertEmpty($wishlistResponse['items_v2']); + $this->assertEmpty($wishlistResponse['items_v2']['items'], 'Wishlist item is not removed'); } /** @@ -64,10 +66,10 @@ public function testDeleteWishlistItemFromWishlist(): void public function testUnauthorizedWishlistItemDelete() { $wishlist = $this->getWishlist(); - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $wishlistItem = $wishlist['customer']['wishlists'][0]['items_v2']['items']; $wishlist2 = $this->getWishlist('customer_two@example.com'); - $wishlist2Id = $wishlist2['customer']['wishlist']['id']; - $query = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id']); + $wishlist2Id = $wishlist2['customer']['wishlists'][0]['id']; + $query = $this->getQuery($wishlist2Id, $wishlistItem[0]['id']); $response = $this->graphQlMutation( $query, [], @@ -75,10 +77,10 @@ public function testUnauthorizedWishlistItemDelete() $this->getHeaderMap('customer_two@example.com') ); self::assertEquals(1, $response['removeProductsFromWishlist']['wishlist']['items_count']); - self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items_v2'], 'empty wish list items'); - self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items_v2']); + self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items_v2']['items'], 'empty wish list items'); + self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items_v2']['items']); self::assertEquals( - 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', + 'The wishlist item with ID "' . $wishlistItem[0]['id'] . '" does not belong to the wishlist', $response['removeProductsFromWishlist']['user_errors'][0]['message'] ); } @@ -109,14 +111,14 @@ private function getHeaderMap(string $username = 'customer@example.com', string * @return string */ private function getQuery( - int $wishlistId, - int $wishlistItemId + string $wishlistId, + string $wishlistItemId ): string { return <<<MUTATION mutation { removeProductsFromWishlist( - wishlistId: {$wishlistId}, - wishlistItemsIds: [{$wishlistItemId}] + wishlistId: "{$wishlistId}", + wishlistItemsIds: ["{$wishlistItemId}"] ) { user_errors { code @@ -127,9 +129,7 @@ private function getQuery( sharing_code items_count items_v2 { - id - description - quantity + items {id description quantity product {name sku}} } } } @@ -159,13 +159,20 @@ private function getCustomerWishlistQuery(): string return <<<QUERY query { customer { - wishlist { + wishlists { id items_count + sharing_code + updated_at items_v2 { + items { id quantity description + product { + sku + } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php index 691c06782070f..51ea9b461edaa 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -42,19 +42,21 @@ public function testUpdateSimpleProductFromWishlist(): void $wishlist = $this->getWishlist(); $qty = 5; $description = 'New Description'; - $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $customerWishlist = $wishlist['customer']['wishlists'][0]; + $wishlistId = $customerWishlist['id']; + $wishlistItem = $customerWishlist['items_v2']['items'][0]; $this->assertNotEquals($description, $wishlistItem['description']); $this->assertNotEquals($qty, $wishlistItem['quantity']); - $query = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); + $query = $this->getQuery($wishlistId, $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); $this->assertArrayHasKey('updateProductsInWishlist', $response); $this->assertArrayHasKey('wishlist', $response['updateProductsInWishlist']); + $this->assertEmpty($response['updateProductsInWishlist']['user_errors']); $wishlistResponse = $response['updateProductsInWishlist']['wishlist']; - $this->assertEquals($qty, $wishlistResponse['items_v2'][0]['quantity']); - $this->assertEquals($description, $wishlistResponse['items_v2'][0]['description']); + $this->assertEquals($qty, $wishlistResponse['items_v2']['items'][0]['quantity']); + $this->assertEquals($description, $wishlistResponse['items_v2']['items'][0]['description']); } /** @@ -67,12 +69,13 @@ public function testUpdateSimpleProductFromWishlist(): void public function testUnauthorizedWishlistItemUpdate() { $wishlist = $this->getWishlist(); - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $customerWishlist = $wishlist['customer']['wishlists'][0]; + $wishlistItem = $customerWishlist['items_v2']['items'][0]; $wishlist2 = $this->getWishlist('customer_two@example.com'); - $wishlist2Id = $wishlist2['customer']['wishlist']['id']; + $wishlist2Id = $wishlist2['customer']['wishlists'][0]['id']; $qty = 2; $description = 'New Description'; - $updateWishlistQuery = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id'], $qty, $description); + $updateWishlistQuery = $this->getQuery($wishlist2Id, $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation( $updateWishlistQuery, [], @@ -82,8 +85,9 @@ public function testUnauthorizedWishlistItemUpdate() self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items_v2']); + self::assertNotEmpty($response['updateProductsInWishlist']['user_errors'], 'No user errors'); self::assertEquals( - 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', + 'The wishlist item with ID "' . $wishlistItem['id'] . '" does not belong to the wishlist', $response['updateProductsInWishlist']['user_errors'][0]['message'] ); } @@ -98,11 +102,12 @@ public function testUnauthorizedWishlistItemUpdate() public function testUpdateProductInWishlistWithZeroQty() { $wishlist = $this->getWishlist(); - $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $customerWishlist = $wishlist['customer']['wishlists'][0]; + $wishlistId = $customerWishlist['id']; + $wishlistItem = $customerWishlist['items_v2']['items'][0]; $qty = 0; $description = 'Description for zero quantity'; - $updateWishlistQuery = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); + $updateWishlistQuery = $this->getQuery($wishlistId, $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation($updateWishlistQuery, [], '', $this->getHeaderMap()); self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); @@ -126,16 +131,17 @@ public function testUpdateProductInWishlistWithZeroQty() public function testUpdateProductWithValidQtyAndNoDescription() { $wishlist = $this->getWishlist(); - $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $customerWishlist = $wishlist['customer']['wishlists'][0]; + $wishlistId = $customerWishlist['id']; + $wishlistItem = $customerWishlist['items_v2']['items'][0]; $qty = 2; - $updateWishlistQuery = $this->getQueryWithNoDescription((int) $wishlistId, (int) $wishlistItem['id'], $qty); + $updateWishlistQuery = $this->getQueryWithNoDescription($wishlistId, $wishlistItem['id'], $qty); $response = $this->graphQlMutation($updateWishlistQuery, [], '', $this->getHeaderMap()); self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); - self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items'], 'empty wish list items'); - self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items']); - $itemsInWishlist = $response['updateProductsInWishlist']['wishlist']['items'][0]; - self::assertEquals($qty, $itemsInWishlist['qty']); + self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); + self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items_v2']['items']); + $itemsInWishlist = $response['updateProductsInWishlist']['wishlist']['items_v2']['items'][0]; + self::assertEquals($qty, $itemsInWishlist['quantity']); self::assertEquals('simple-1', $itemsInWishlist['product']['sku']); } @@ -167,15 +173,15 @@ private function getHeaderMap(string $username = 'customer@example.com', string * @return string */ private function getQuery( - int $wishlistId, - int $wishlistItemId, + string $wishlistId, + string $wishlistItemId, int $qty, string $description ): string { return <<<MUTATION mutation { updateProductsInWishlist( - wishlistId: {$wishlistId}, + wishlistId: "{$wishlistId}", wishlistItems: [ { wishlist_item_id: "{$wishlistItemId}" @@ -193,9 +199,11 @@ private function getQuery( sharing_code items_count items_v2 { - id + items{ + id description quantity + } } } } @@ -213,14 +221,14 @@ private function getQuery( * @return string */ private function getQueryWithNoDescription( - int $wishlistId, - int $wishlistItemId, + string $wishlistId, + string $wishlistItemId, int $qty ): string { return <<<MUTATION mutation { updateProductsInWishlist( - wishlistId: {$wishlistId}, + wishlistId: "{$wishlistId}", wishlistItems: [ { wishlist_item_id: "{$wishlistItemId}" @@ -237,10 +245,12 @@ private function getQueryWithNoDescription( id sharing_code items_count - items { - id - qty - product{sku name} + items_v2 { + items{ + id + quantity + product {sku name} + } } } } @@ -271,13 +281,20 @@ private function getCustomerWishlistQuery(): string return <<<QUERY query { customer { - wishlist { + wishlists { id items_count + sharing_code + updated_at items_v2 { + items { id quantity description + product { + sku + } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php index 11e07d081636e..efa7341c36a40 100644 --- a/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GroupedProduct/Api/ProductLinkRepositoryTest.php @@ -7,28 +7,44 @@ namespace Magento\GroupedProduct\Api; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Indexer\Model\Config; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Webapi\Rest\Request; class ProductLinkRepositoryTest extends \Magento\TestFramework\TestCase\WebapiAbstract { const SERVICE_NAME = 'catalogProductLinkRepositoryV1'; const SERVICE_VERSION = 'V1'; const RESOURCE_PATH = '/V1/products/'; + const SERVICE_NAME_SEARCH = 'searchV1'; + const RESOURCE_PATH_SEARCH = '/V1/search/'; /** * @var \Magento\Framework\ObjectManagerInterface */ protected $objectManager; + /** + * @var array + */ + private $indexersState; + + /** + * @var mixed + */ + private $indexerRegistry; + protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); + $this->indexerRegistry = $this->objectManager->get(IndexerRegistry::class); } /** * @magentoApiDataFixture Magento/Catalog/_files/product_simple_duplicated.php * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped.php */ - public function testSave() + public function testSave(): void { $productSku = 'grouped-product'; $linkType = 'associated'; @@ -46,7 +62,7 @@ public function testSave() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . $productSku . '/links', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -64,4 +80,106 @@ public function testSave() }); $this->assertEquals($productData, $actual[2]); } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped.php + */ + public function testLinkWithScheduledIndex(): void + { + $this->setIndexScheduled(); + $productSkuGrouped = 'grouped-product'; + $productSimple = 'simple-1'; + $linkType = 'associated'; + $productData = [ + 'sku' => $productSkuGrouped, + 'link_type' => $linkType, + 'linked_product_type' => 'simple', + 'linked_product_sku' => $productSimple, + 'position' => 3, + 'extension_attributes' => [ + 'qty' => (float) 300.0000, + ], + ]; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . $productSkuGrouped . '/links', + 'httpMethod' => Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + $this->_webApiCall($serviceInfo, ['entity' => $productData]); + + $searchCriteria = $this->buildSearchCriteria($productSimple); + $serviceInfo = $this->buildSearchServiceInfo($searchCriteria); + $response = $this->_webApiCall($serviceInfo, $searchCriteria); + $this->assertArrayHasKey('search_criteria', $response); + $this->assertArrayHasKey('items', $response); + $this->assertGreaterThan(1, count($response['items'])); + $this->assertGreaterThan(0, $response['items'][0]['id']); + $this->restoreIndexMode(); + } + + /** + * @param string $productSku + * @return array + */ + private function buildSearchCriteria(string $productSku): array + { + return [ + 'searchCriteria' => [ + 'request_name' => 'quick_search_container', + 'filter_groups' => [ + [ + 'filters' => [ + [ + 'field' => 'search_term', + 'value' => $productSku, + ] + ] + ] + ] + ] + ]; + } + + /** + * @param array $searchCriteria + * @return array + */ + private function buildSearchServiceInfo(array $searchCriteria): array + { + return [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH_SEARCH . '?' . http_build_query($searchCriteria), + 'httpMethod' => Request::HTTP_METHOD_GET + ], + 'soap' => [ + 'service' => self::SERVICE_NAME_SEARCH, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME_SEARCH . 'Search' + ] + ]; + } + + private function setIndexScheduled(): void + { + $indexerListIds = $this->objectManager->get(Config::class)->getIndexers(); + foreach ($indexerListIds as $indexerId) { + $indexer = $this->indexerRegistry->get($indexerId['indexer_id']); + $this->indexersState[$indexerId['indexer_id']] = $indexer->isScheduled(); + $indexer->setScheduled(true); + } + } + + private function restoreIndexMode(): void + { + foreach ($this->indexersState as $indexerId => $state) { + $this->indexerRegistry->get($indexerId)->setScheduled($state); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartAddingItemsTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartAddingItemsTest.php index 7900ae45e2f3d..bb2a4e68212cf 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartAddingItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/CartAddingItemsTest.php @@ -7,6 +7,11 @@ namespace Magento\Quote\Api; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -15,13 +20,22 @@ class CartAddingItemsTest extends WebapiAbstract { /** - * @var \Magento\TestFramework\ObjectManager + * @var ObjectManager */ protected $objectManager; + /** + * @var ProductResource + */ + private $productResource; + + /** + * @inheritDoc + */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productResource = $this->objectManager->get(ProductResource::class); } /** @@ -36,9 +50,9 @@ public function testPriceForCreatingQuoteFromEmptyCart() $this->_markTestAsRestOnly(); // Get customer ID token - /** @var \Magento\Integration\Api\CustomerTokenServiceInterface $customerTokenService */ + /** @var CustomerTokenServiceInterface $customerTokenService */ $customerTokenService = $this->objectManager->create( - \Magento\Integration\Api\CustomerTokenServiceInterface::class + CustomerTokenServiceInterface::class ); $token = $customerTokenService->createCustomerAccessToken( 'customer_one_address@test.com', @@ -49,7 +63,7 @@ public function testPriceForCreatingQuoteFromEmptyCart() $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/carts/mine', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, 'token' => $token ] ]; @@ -58,13 +72,6 @@ public function testPriceForCreatingQuoteFromEmptyCart() $this->assertGreaterThan(0, $quoteId); // Adding item to the cart - $serviceInfoForAddingProduct = [ - 'rest' => [ - 'resourcePath' => '/V1/carts/mine/items', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, - 'token' => $token - ] - ]; $requestData = [ 'cartItem' => [ 'quote_id' => $quoteId, @@ -72,7 +79,7 @@ public function testPriceForCreatingQuoteFromEmptyCart() 'qty' => 1 ] ]; - $item = $this->_webApiCall($serviceInfoForAddingProduct, $requestData); + $item = $this->_webApiCall($this->getServiceInfoAddToCart($token), $requestData); $this->assertNotEmpty($item); $this->assertEquals(10, $item['price']); @@ -80,7 +87,7 @@ public function testPriceForCreatingQuoteFromEmptyCart() $serviceInfoForGettingPaymentInfo = [ 'rest' => [ 'resourcePath' => '/V1/carts/mine/payment-information', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, 'token' => $token ] ]; @@ -92,4 +99,137 @@ public function testPriceForCreatingQuoteFromEmptyCart() $quote->load($quoteId); $quote->delete(); } + + /** + * Test qty for cart after adding grouped product with custom qty. + * + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer_one_address.php + * @return void + */ + public function testAddToCartGroupedCustomQuantity(): void + { + $this->_markTestAsRestOnly(); + + $firstProductId = $this->productResource->getIdBySku('simple_11'); + $secondProductId = $this->productResource->getIdBySku('simple_22'); + $qtyData = [$firstProductId => 2, $secondProductId => 4]; + + // Get customer ID token + /** @var CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = $this->objectManager->create(CustomerTokenServiceInterface::class); + $token = $customerTokenService->createCustomerAccessToken( + 'customer_one_address@test.com', + 'password' + ); + + // Creating empty cart for registered customer. + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/carts/mine', + 'httpMethod' => Request::HTTP_METHOD_POST, + 'token' => $token + ] + ]; + + $quoteId = $this->_webApiCall($serviceInfo, ['customerId' => 999]); // customerId 999 will get overridden + $this->assertGreaterThan(0, $quoteId); + + // Adding item to the cart + $productOptionData = [ + 'extension_attributes' => [ + 'grouped_options' => [ + ['id' => $firstProductId, 'qty' => $qtyData[$firstProductId]], + ['id' => $secondProductId, 'qty' => $qtyData[$secondProductId]], + ] + ] + ]; + $requestData = [ + 'cartItem' => [ + 'quote_id' => $quoteId, + 'sku' => 'grouped', + 'qty' => 1, + 'product_option' => $productOptionData + ] + ]; + $response = $this->_webApiCall($this->getServiceInfoAddToCart($token), $requestData); + $this->assertArrayHasKey('product_option', $response); + $this->assertEquals($response['product_option'], $productOptionData); + + /** @var CartRepositoryInterface $cartRepository */ + $cartRepository = $this->objectManager->get(CartRepositoryInterface::class); + $quote = $cartRepository->get($quoteId); + + foreach ($quote->getAllItems() as $item) { + $this->assertEquals($qtyData[$item->getProductId()], $item->getQty()); + } + } + + /** + * Test adding grouped product when qty for grouped_options not specified. + * + * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple.php + * @magentoApiDataFixture Magento/Customer/_files/customer_one_address.php + * @return void + */ + public function testAddToCartGroupedCustomQuantityNotAllParamsSpecified(): void + { + $this->_markTestAsRestOnly(); + + $productId = $this->productResource->getIdBySku('simple_11'); + + // Get customer ID token + /** @var CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = $this->objectManager->create(CustomerTokenServiceInterface::class); + $token = $customerTokenService->createCustomerAccessToken( + 'customer_one_address@test.com', + 'password' + ); + + // Creating empty cart for registered customer. + $serviceInfo = [ + 'rest' => ['resourcePath' => '/V1/carts/mine', 'httpMethod' => Request::HTTP_METHOD_POST, 'token' => $token] + ]; + + $quoteId = $this->_webApiCall($serviceInfo, ['customerId' => 999]); // customerId 999 will get overridden + $this->assertGreaterThan(0, $quoteId); + + // Adding item to the cart + $requestData = [ + 'cartItem' => [ + 'quote_id' => $quoteId, + 'sku' => 'grouped', + 'qty' => 1, + 'product_option' => [ + 'extension_attributes' => [ + 'grouped_options' => [ + ['id' => $productId], + ] + ] + ] + ] + ]; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Please specify id and qty for grouped options.'); + + $this->_webApiCall($this->getServiceInfoAddToCart($token), $requestData); + } + + /** + * Returns service info add to cart + * + * @param string $token + * @return array + */ + private function getServiceInfoAddToCart(string $token): array + { + return [ + 'rest' => [ + 'resourcePath' => '/V1/carts/mine/items', + 'httpMethod' => Request::HTTP_METHOD_POST, + 'token' => $token + ] + ]; + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php index 373ad64ba39d4..1054706819e95 100644 --- a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestCartItemRepositoryTest.php @@ -5,8 +5,13 @@ */ namespace Magento\Quote\Api; +use Magento\Catalog\Model\Product; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Model\Stock; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -40,14 +45,14 @@ protected function setUp(): void */ public function testGetList() { - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $cartId = $quote->getId(); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id @@ -92,17 +97,17 @@ public function testGetList() */ public function testAddItem() { - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class)->load(2); + /** @var Product $product */ + $product = $this->objectManager->create(Product::class)->load(2); $productSku = $product->getSku(); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_1', 'reserved_order_id'); $cartId = $quote->getId(); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id @@ -141,20 +146,20 @@ public function testAddItem() */ public function testRemoveItem() { - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $cartId = $quote->getId(); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id $cartId = $quoteIdMask->getMaskedId(); - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); + $product = $this->objectManager->create(Product::class); $productId = $product->getIdBySku('simple_one'); $product->load($productId); $itemId = $quote->getItemByProduct($product)->getId(); @@ -175,7 +180,7 @@ public function testRemoveItem() "itemId" => $itemId, ]; $this->assertTrue($this->_webApiCall($serviceInfo, $requestData)); - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $this->assertFalse($quote->hasProductId($productId)); } @@ -189,20 +194,20 @@ public function testRemoveItem() public function testUpdateItem(array $stockData, string $errorMessage = null) { $this->updateStockData('simple_one', $stockData); - /** @var \Magento\Quote\Model\Quote $quote */ - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $cartId = $quote->getId(); - /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ - $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->load($cartId, 'quote_id'); //Use masked cart Id $cartId = $quoteIdMask->getMaskedId(); - $product = $this->objectManager->create(\Magento\Catalog\Model\Product::class); + $product = $this->objectManager->create(Product::class); $productId = $product->getIdBySku('simple_one'); $product->load($productId); $itemId = $quote->getItemByProduct($product)->getId(); @@ -229,7 +234,7 @@ public function testUpdateItem(array $stockData, string $errorMessage = null) $this->expectExceptionMessage($errorMessage); } $this->_webApiCall($serviceInfo, $requestData); - $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + $quote = $this->objectManager->create(Quote::class); $quote->load('test_order_item_with_items', 'reserved_order_id'); $this->assertTrue($quote->hasProductId(1)); $item = $quote->getItemByProduct($product); @@ -237,6 +242,62 @@ public function testUpdateItem(array $stockData, string $errorMessage = null) $this->assertEquals($itemId, $item->getItemId()); } + /** + * Verifies that store id for quote and quote item is being changed accordingly to the requested store code + * + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + * @magentoApiDataFixture Magento/Store/_files/second_store.php + */ + public function testUpdateItemWithChangingStoreId() + { + /** @var Quote $quote */ + $quote = $this->objectManager->create(Quote::class); + $quote->load('test_order_item_with_items', 'reserved_order_id'); + $cartId = $quote->getId(); + + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cartId, 'quote_id'); + $cartId = $quoteIdMask->getMaskedId(); + + $product = $this->objectManager->create(Product::class); + $productId = $product->getIdBySku('simple'); + $product->load($productId); + $itemId = $quote->getItemByProduct($product)->getId(); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . $cartId . '/items/' . $itemId, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + + $requestData['cartItem']['qty'] = 5; + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $requestData['cartItem'] += [ + 'quote_id' => $cartId, + 'itemId' => $itemId, + ]; + } + $this->_webApiCall($serviceInfo, $requestData, null, 'fixture_second_store'); + $quote = $this->objectManager->create(Quote::class); + $quote->load('test_order_item_with_items', 'reserved_order_id'); + $this->assertTrue($quote->hasProductId(1)); + $item = $quote->getItemByProduct($product); + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeId = $storeManager->getStore('fixture_second_store') + ->getId(); + $this->assertEquals($storeId, $quote->getStoreId()); + $this->assertEquals($storeId, $item->getStoreId()); + } + /** * @return array */ diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/adminhtml/csp_whitelist.xml b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/adminhtml/csp_whitelist.xml new file mode 100644 index 0000000000000..53e8775761596 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/adminhtml/csp_whitelist.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp/etc/csp_whitelist.xsd"> + <policies> + <policy id="object-src"> + <values> + <value id="example-base" type="host">example.magento.com</value> + <value id="mage-base" type="host">https://admin.magento.com</value> + </values> + </policy> + <policy id="media-src"> + <values> + <value id="example-base" type="host">example.magento.com</value> + <value id="mage-base" type="host">https://admin.magento.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml new file mode 100644 index 0000000000000..9808d90ace49c --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMview/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleMview"/> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml b/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml new file mode 100644 index 0000000000000..1cabda7a626bf --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMview/etc/mview.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Mview/etc/mview.xsd"> + <view id="test_view_with_additional_columns" class="Magento\Framework\Indexer\Action\Dummy" group="indexer"> + <subscriptions> + <table name="test_mview_table" entity_column="entity_id"> + <additionalColumns> + <column name="additional_column" cl_name="test_additional_column" /> + </additionalColumns> + </table> + </subscriptions> + </view> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleMview/registration.php b/dev/tests/integration/_files/Magento/TestModuleMview/registration.php new file mode 100644 index 0000000000000..5c5453c1bd413 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleMview/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleMview') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleMview', __DIR__); +} diff --git a/dev/tests/integration/testsuite/Magento/Backend/App/Area/FrontNameResolverTest.php b/dev/tests/integration/testsuite/Magento/Backend/App/Area/FrontNameResolverTest.php new file mode 100644 index 0000000000000..979e8db19efb9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/App/Area/FrontNameResolverTest.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backend\App\Area; + +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * @magentoAppArea adminhtml + */ +class FrontNameResolverTest extends TestCase +{ + /** + * @var \Magento\Framework\ObjectManagerInterface + */ + protected $objectManager; + + /** + * @var FrontNameResolver + */ + protected $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->create( + FrontNameResolver::class + ); + $_SERVER['HTTP_HOST'] = 'localhost'; + } + + /** + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store web/unsecure/base_url http://example.com/ + */ + public function testIsHostBackend() + { + $this->assertTrue($this->model->isHostBackend()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php index 7e94484961f9e..56a07034bd490 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php @@ -6,15 +6,20 @@ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\UpdateHandler; use Magento\Framework\App\Request\DataPersistorInterface; use Magento\Framework\Registry; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; /** * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ContentTest extends \PHPUnit\Framework\TestCase { @@ -35,6 +40,16 @@ class ContentTest extends \PHPUnit\Framework\TestCase */ private $dataPersistor; + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** * @inheritdoc */ @@ -51,6 +66,8 @@ protected function setUp(): void $this->block->setElement($gallery); $this->registry = Bootstrap::getObjectManager()->get(Registry::class); $this->dataPersistor = Bootstrap::getObjectManager()->get(DataPersistorInterface::class); + $this->storeRepository = Bootstrap::getObjectManager()->create(StoreRepositoryInterface::class); + $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); } public function testGetUploader() @@ -120,6 +137,119 @@ public function getImagesAndImageTypesDataProvider() ]; } + /** + * Tests images positions in store view + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @dataProvider imagesPositionStoreViewDataProvider + * @param string $addFromStore + * @param array $newImages + * @param string $viewFromStore + * @param array $expectedImages + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testImagesPositionStoreView( + string $addFromStore, + array $newImages, + string $viewFromStore, + array $expectedImages + ): void { + $storeId = (int)$this->storeRepository->get($addFromStore)->getId(); + $product = $this->getProduct($storeId); + $images = $product->getData('media_gallery')['images']; + $images = array_merge($images, $newImages); + $product->setData('media_gallery', ['images' => $images]); + $updateHandler = Bootstrap::getObjectManager()->create(UpdateHandler::class); + $updateHandler->execute($product); + $storeId = (int)$this->storeRepository->get($viewFromStore)->getId(); + $product = $this->getProduct($storeId); + $this->registry->register('current_product', $product); + $actualImages = array_map( + function ($item) { + return [ + 'file' => $item['file'], + 'label' => $item['label'], + 'position' => $item['position'], + ]; + }, + json_decode($this->block->getImagesJson(), true) + ); + $this->assertEquals($expectedImages, array_values($actualImages)); + } + + /** + * @return array[] + */ + public function imagesPositionStoreViewDataProvider(): array + { + return [ + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'default', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'file' => '/m/a/magento_small_image.jpg', + 'label' => null, + 'position' => 2, + ], + ] + ], + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'file' => '/m/a/magento_small_image.jpg', + 'label' => 'New Image Alt Text', + 'position' => 2, + ], + ] + ] + ]; + } + + /** + * Returns product for testing. + * + * @param int $storeId + * @param string $sku + * @return ProductInterface + */ + private function getProduct(int $storeId = Store::DEFAULT_STORE_ID, string $sku = 'simple'): ProductInterface + { + return $this->productRepository->get($sku, false, $storeId, true); + } + /** * Prepare product, and set it to registry and data persistor. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php index b57969280cdf3..bcec3168c7885 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Gallery\UpdateHandler; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\View\LayoutInterface; @@ -392,6 +393,107 @@ public function galleryImagesOnStoreViewDataProvider(): array ]; } + /** + * Tests images positions in store view + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/web/url/catalog_media_url_format image_optimization_parameters + * @dataProvider imagesPositionStoreViewDataProvider + * @param string $addFromStore + * @param array $newImages + * @param string $viewFromStore + * @param array $expectedImages + * @return void + */ + public function testImagesPositionStoreView( + string $addFromStore, + array $newImages, + string $viewFromStore, + array $expectedImages + ): void { + $storeId = (int)$this->storeRepository->get($addFromStore)->getId(); + $product = $this->getProduct($storeId); + $images = $product->getData('media_gallery')['images']; + $images = array_merge($images, $newImages); + $product->setData('media_gallery', ['images' => $images]); + $updateHandler = Bootstrap::getObjectManager()->create(UpdateHandler::class); + $updateHandler->execute($product); + $storeId = (int)$this->storeRepository->get($viewFromStore)->getId(); + $product = $this->getProduct($storeId); + $this->block->setData('product', $product); + $actualImages = array_map( + function ($item) { + return [ + 'img' => parse_url($item['img'], PHP_URL_PATH), + 'caption' => $item['caption'], + 'position' => $item['position'], + ]; + }, + $this->serializer->unserialize($this->block->getGalleryImagesJson()) + ); + $this->assertEquals($expectedImages, array_values($actualImages)); + } + + /** + * @return array[] + */ + public function imagesPositionStoreViewDataProvider(): array + { + return [ + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'default', + [ + [ + 'img' => '/media/catalog/product/m/a/magento_image.jpg', + 'caption' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'img' => '/media/catalog/product/m/a/magento_small_image.jpg', + 'caption' => 'Simple Product', + 'position' => 2, + ], + ] + ], + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'fixture_second_store', + [ + [ + 'img' => '/media/catalog/product/m/a/magento_image.jpg', + 'caption' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'img' => '/media/catalog/product/m/a/magento_small_image.jpg', + 'caption' => 'New Image Alt Text', + 'position' => 2, + ], + ] + ] + ]; + } + /** * Updates product gallery images and saves product. * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/Type/DateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/Type/DateTest.php new file mode 100644 index 0000000000000..91a54d8fc13fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/Type/DateTest.php @@ -0,0 +1,324 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Product\View\Options\Type; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Pricing\Render; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ +class DateTest extends TestCase +{ + /** + * @var Date + */ + private $block; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @var ProductHelper + */ + private $productHelper; + + /** + * @var ResolverInterface + */ + private $localeResolver; + + /** + * @var string + */ + private $defaultLocale; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->productHelper = $objectManager->get(ProductHelper::class); + $this->dataObjectFactory = $objectManager->get(DataObjectFactory::class); + $layout = $objectManager->get(LayoutInterface::class); + $this->localeResolver = $objectManager->get(ResolverInterface::class); + $this->defaultLocale = $this->localeResolver->getLocale(); + $this->block = $layout->createBlock( + Date::class, + 'product.info.options.date', + [ + 'data' => [ + 'template' => 'Magento_Catalog::product/view/options/type/date.phtml' + ] + ] + ); + $layout->createBlock( + Render::class, + 'product.price.render.default', + [ + 'data' => [ + 'price_render_handle' => 'catalog_product_prices', + 'use_link_for_as_low_as' => true, + ], + ] + ); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->localeResolver->setLocale($this->defaultLocale); + parent::tearDown(); + } + + /** + * @magentoAppArea frontend + * @param array $data + * @param array $expected + * @dataProvider toHtmlWithDropDownDataProvider + */ + public function testToHtmlWithDropDown(array $data, array $expected): void + { + $this->prepareBlock($data); + $this->assertXPaths($expected); + } + + /** + * @magentoAppArea frontend + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @param array $data + * @param array $expected + * @param string|null $locale + * @dataProvider toHtmlWithCalendarDataProvider + */ + public function testToHtmlWithCalendar(array $data, array $expected, ?string $locale = null): void + { + if ($locale) { + $this->localeResolver->setLocale($locale); + } + $this->prepareBlock($data); + $this->assertXPaths($expected); + } + + /** + * @param array $expected + */ + private function assertXPaths(array $expected): void + { + $html = $this->block->toHtml(); + $domXpath = new \DOMXPath($this->getHtmlDocument($html)); + foreach ($expected as $xpath => $value) { + $xpath = strtr($xpath, ['{id}' => $this->block->getOption()->getId()]); + $nodes = $domXpath->query(strtr($xpath, ['{id}' => $this->block->getOption()->getId()])); + $this->assertEquals(1, $nodes->count(), 'Cannot find element \'' . $xpath . '"\' in the HTML'); + $this->assertEquals($value, $nodes->item(0)->getAttribute('value')); + } + } + + /** + * @param array $data + */ + private function prepareBlock(array $data): void + { + /** @var Product $product */ + $product = $this->productRepository->get('simple'); + $this->block->setProduct($product); + $option = $this->getDateTimeOption($product); + $this->block->setOption($option); + $buyRequest = $this->dataObjectFactory->create(); + $buyRequest->setData( + [ + 'qty' => 1, + 'options' => [ + $option->getId() => $data + ], + ] + ); + $this->productHelper->prepareProductOptions($product, $buyRequest); + } + + /** + * @param Product $product + * @return Option|null + */ + private function getDateTimeOption(Product $product): ?Option + { + $option = null; + foreach ($product->getOptions() as $customOption) { + if ($customOption->getType() === Option::OPTION_TYPE_DATE_TIME) { + $option = $customOption; + break; + } + } + return $option; + } + + /** + * @param string $source + * @return \DOMDocument + */ + private function getHtmlDocument(string $source): \DOMDocument + { + $page =<<<HTML +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Title + + +$source + + +HTML; + $domDocument = new \DOMDocument('1.0', 'UTF-8'); + libxml_use_internal_errors(true); + $domDocument->loadHTML($page); + libxml_use_internal_errors(false); + return $domDocument; + } + + /** + * @return array + */ + public function toHtmlWithDropDownDataProvider(): array + { + return [ + [ + [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//select[@id="options_{id}_year"]/option[@selected]' => '2020', + '//select[@id="options_{id}_month"]/option[@selected]' => '3', + '//select[@id="options_{id}_day"]/option[@selected]' => '5', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ] + ], + [ + [ + 'date' => '09/30/2022', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//select[@id="options_{id}_year"]/option[@selected]' => '2020', + '//select[@id="options_{id}_month"]/option[@selected]' => '9', + '//select[@id="options_{id}_day"]/option[@selected]' => '30', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ] + ] + ]; + } + + /** + * @return array + */ + public function toHtmlWithCalendarDataProvider(): array + { + return [ + [ + [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//input[@id="options_{id}_date"]' => '3/5/2020', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ] + ], + [ + [ + 'date' => '09/30/2022', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//input[@id="options_{id}_date"]' => '9/30/2020', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ] + ], + [ + [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//input[@id="options_{id}_date"]' => '05/03/2020', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ], + 'fr_FR' + ], + [ + [ + 'date' => '09/30/2022', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + [ + '//input[@id="options_{id}_date"]' => '30/09/2020', + '//select[@id="options_{id}_hour"]/option[@selected]' => '2', + '//select[@id="options_{id}_minute"]/option[@selected]' => '15', + '//select[@id="options_{id}_day_part"]/option[@selected]' => 'am', + ], + 'fr_FR' + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index 7032199e9fc4c..8b18f89542494 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -138,9 +138,6 @@ public function testSaveActionAndDuplicateWithUrlPathAttribute() $urlPathAttribute = $product->getCustomAttribute('url_path'); $this->assertEquals($urlPathAttribute->getValue(), $product->getSku()); - // clean cache - CacheCleaner::cleanAll(); - // dispatch Save&Duplicate action and check it $this->assertSaveAndDuplicateAction($product); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ConfigTest.php index 36379adcee601..8320ef979180f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ConfigTest.php @@ -31,7 +31,6 @@ protected function setUp(): void public function testGetEntityAttributeCodes() { $entityType = 'catalog_product'; - CacheCleaner::cleanAll(); $this->assertEquals( $this->config->getEntityAttributeCodes($entityType), $this->config->getEntityAttributeCodes($entityType) @@ -42,7 +41,6 @@ public function testGetAttribute() { $entityType = 'catalog_product'; $attributeCode = 'color'; - CacheCleaner::cleanAll(); $this->assertEquals( $this->config->getAttribute($entityType, $attributeCode), $this->config->getAttribute($entityType, $attributeCode) @@ -52,7 +50,6 @@ public function testGetAttribute() public function testGetEntityType() { $entityType = 'catalog_product'; - CacheCleaner::cleanAll(); $this->assertEquals( $this->config->getEntityType($entityType), $this->config->getEntityType($entityType) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php index 8e11efa8790cf..5cfa07cf5d402 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Category/ProductTest.php @@ -245,36 +245,6 @@ public function testCatalogCategoryProductIndexInvalidateAfterDelete(): void $this->assertEquals(StateInterface::STATUS_INVALID, $status); } - /** - * Test invalidate reindex after change product position on category - * - * @magentoAppArea adminhtml - * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php - * - * @return void - */ - public function testCatalogCategoryProductIndexInvalidateAfterChangeProductPosition(): void - { - $this->indexer->setScheduled(true); - $indexerShouldBeValid = $this->indexer->isValid(); - - $category = $this->getCategoryByName->execute('Category 999'); - - $category->setPostedProducts([ - $this->productResource->getIdBySku('simple1000') => 1, - $this->productResource->getIdBySku('simple1001') => 2 - ]); - - $this->categoryResource->save($category); - - $state = $this->indexer->getState(); - $state->loadByIndexer($this->indexer->getId()); - $status = $state->getStatus(); - - $this->assertTrue($indexerShouldBeValid); - $this->assertEquals(StateInterface::STATUS_INVALID, $status); - } - /** * @param int $count * @return Category[] diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Source/CountryofmanufactureTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Source/CountryofmanufactureTest.php index 33e82e9f6ddcc..8b8f54af2d387 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Source/CountryofmanufactureTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Attribute/Source/CountryofmanufactureTest.php @@ -24,7 +24,6 @@ protected function setUp(): void public function testGetAllOptions() { - CacheCleaner::cleanAll(); $allOptions = $this->model->getAllOptions(); $cachedAllOptions = $this->model->getAllOptions(); $this->assertEquals($allOptions, $cachedAllOptions); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 481ec6aeac0f2..d20bf2907c780 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -593,6 +593,102 @@ public function updateImageDataProvider(): array ]; } + /** + * Tests that images are added correctly + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Store/_files/second_store.php + * @dataProvider addImagesDataProvider + * @param string $addFromStore + * @param array $newImages + * @param string $viewFromStore + * @param array $expectedImages + * @param array $select + * @return void + */ + public function testAddImages( + string $addFromStore, + array $newImages, + string $viewFromStore, + array $expectedImages, + array $select = ['file', 'label', 'position'] + ): void { + $storeId = (int)$this->storeRepository->get($addFromStore)->getId(); + $product = $this->getProduct($storeId); + $images = $product->getData('media_gallery')['images']; + $images = array_merge($images, $newImages); + $product->setData('media_gallery', ['images' => $images]); + $this->updateHandler->execute($product); + $storeId = (int)$this->storeRepository->get($viewFromStore)->getId(); + $product = $this->getProduct($storeId); + $actualImages = array_map( + function (\Magento\Framework\DataObject $item) use ($select) { + return $item->toArray($select); + }, + $product->getMediaGalleryImages()->getItems() + ); + $this->assertEquals($expectedImages, array_values($actualImages)); + } + + /** + * @return array[] + */ + public function addImagesDataProvider(): array + { + return [ + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'default', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'file' => '/m/a/magento_small_image.jpg', + 'label' => null, + 'position' => 2, + ], + ] + ], + [ + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_small_image.jpg', + 'position' => 2, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ] + ], + 'fixture_second_store', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1, + ], + [ + 'file' => '/m/a/magento_small_image.jpg', + 'label' => 'New Image Alt Text', + 'position' => 2, + ], + ] + ] + ]; + } + /** * Check product image link and product image exist * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php index 5b9c7b267f188..7d9e9a48093cb 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/Entity/AttributeTest.php @@ -52,7 +52,6 @@ class AttributeTest extends \PHPUnit\Framework\TestCase */ protected function setUp(): void { - CacheCleaner::cleanAll(); $this->objectManager = Bootstrap::getObjectManager(); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $this->attributeRepository = $this->objectManager->get(AttributeRepository::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php index c57e981f772de..66e117a61ed2c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php @@ -10,14 +10,21 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category as CategoryModel; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\UrlInterface; +use Magento\Indexer\Cron\UpdateMview; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -26,6 +33,7 @@ * Tests category resource model * * @see \Magento\Catalog\Model\ResourceModel\Category + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategoryTest extends TestCase { @@ -54,6 +62,11 @@ class CategoryTest extends TestCase /** @var WriteInterface */ private $mediaDirectory; + /** + * @var ProductResource + */ + private $productResource; + /** * @inheritdoc */ @@ -68,6 +81,7 @@ protected function setUp(): void $this->categoryCollection = $this->objectManager->get(CategoryCollectionFactory::class)->create(); $this->filesystem = $this->objectManager->get(Filesystem::class); $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->productResource = Bootstrap::getObjectManager()->get(ProductResource::class); } /** @@ -116,6 +130,128 @@ public function testAddImageForCategory(): void $this->assertFileExists($this->mediaDirectory->getAbsolutePath($imageRelativePath)); } + /** + * Test that adding or removing products in a category should not trigger full reindex in scheduled update mode + * + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Catalog/_files/catalog_category_product_reindex_all.php + * @magentoDataFixture Magento/Catalog/_files/catalog_product_category_reindex_all.php + * @magentoDataFixture Magento/Catalog/_files/enable_catalog_product_reindex_schedule.php + * @dataProvider catalogProductChangesWithScheduledUpdateDataProvider + * @param array $products + * @return void + */ + public function testCatalogProductChangesWithScheduledUpdate(array $products): void + { + // products are ordered by entity_id DESC because their positions are same and equal to 0 + $initialProducts = ['simple1002', 'simple1001', 'simple1000']; + $defaultStoreId = (int) $this->storeManager->getDefaultStoreView()->getId(); + $category = $this->getCategory(['name' => 'Category 999']); + $expectedProducts = array_keys($products); + $productIdsBySkus = $this->productResource->getProductsIdsBySkus($expectedProducts); + $postedProducts = []; + foreach ($products as $sku => $position) { + $postedProducts[$productIdsBySkus[$sku]] = $position; + } + $category->setPostedProducts($postedProducts); + $this->categoryResource->save($category); + // Indices should not be invalidated when adding/removing/reordering products in a category. + $categoryProductIndexer = $this->getIndexer(CategoryProductIndexer::INDEXER_ID); + $this->assertTrue( + $categoryProductIndexer->isValid(), + '"Indexed category/products association" indexer should not be invalidated.' + ); + $productCategoryIndexer = $this->getIndexer(ProductCategoryIndexer::INDEXER_ID); + $this->assertTrue( + $productCategoryIndexer->isValid(), + '"Indexed product/categories association" indexer should not be invalidated.' + ); + // catalog products is not update until partial reindex occurs + $collection = $this->getCategoryProducts($category, $defaultStoreId); + $this->assertEquals($initialProducts, $collection->getColumnValues('sku')); + // Execute MVIEW cron handler for cron job "indexer_update_all_views" + /** @var $mViewCron UpdateMview */ + $mViewCron = $this->objectManager->create(UpdateMview::class); + $mViewCron->execute(); + $collection = $this->getCategoryProducts($category, $defaultStoreId); + $this->assertEquals($expectedProducts, $collection->getColumnValues('sku')); + } + + /** + * @return array + */ + public function catalogProductChangesWithScheduledUpdateDataProvider(): array + { + return [ + 'change products position' => [ + [ + 'simple1002' => 1, + 'simple1000' => 2, + 'simple1001' => 3, + ] + ], + 'Add new product' => [ + [ + 'simple1002' => 1, + 'simple1000' => 2, + 'simple-1' => 3, + 'simple1001' => 4, + ] + ], + 'Delete product' => [ + [ + 'simple1002' => 1, + 'simple1000' => 2, + ] + ] + ]; + } + + /** + * @param CategoryModel $category + * @param int $defaultStoreId + * @return ProductCollection + */ + private function getCategoryProducts(CategoryModel $category, int $defaultStoreId) + { + /** @var ProductCollection $collection */ + $collection = $this->objectManager->create(ProductCollection::class); + $collection->setStoreId($defaultStoreId); + $collection->addCategoryFilter($category); + $collection->addAttributeToSort('position'); + return $collection; + } + + /** + * @param array $filters + * @return CategoryModel + */ + private function getCategory(array $filters): CategoryModel + { + /** @var CategoryCollection $categoryCollection */ + $categoryCollection = $this->objectManager->create(CategoryCollection::class); + foreach ($filters as $field => $value) { + $categoryCollection->addFieldToFilter($field, $value); + } + + return $categoryCollection->getFirstItem(); + } + + /** + * @param string $indexerId + * @return IndexerInterface + */ + private function getIndexer(string $indexerId): IndexerInterface + { + /** @var IndexerRegistry $indexerRegistry */ + $indexerRegistry = $this->objectManager->get(IndexerRegistry::class); + return $indexerRegistry->get($indexerId); + } + /** * Prepare image url for image data * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index 8ad346af068b4..d5e7d94ec0cae 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -39,7 +39,6 @@ public function testModifyMeta() { $inputMeta = include __DIR__ . '/_files/input_meta_for_categories.php'; $expectedCategories = include __DIR__ . '/_files/expected_categories.php'; - CacheCleaner::cleanAll(); $this->assertCategoriesInMeta($expectedCategories, $this->object->modifyMeta($inputMeta)); // Verify cached data $this->assertCategoriesInMeta($expectedCategories, $this->object->modifyMeta($inputMeta)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_product_reindex_all.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_product_reindex_all.php new file mode 100644 index 0000000000000..3dfba9266cddc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_product_reindex_all.php @@ -0,0 +1,16 @@ +get(IndexerRegistry::class); + +$model = $indexRegistry->get(CategoryProductIndexer::INDEXER_ID); +$model->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_product_category_reindex_all.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_product_category_reindex_all.php new file mode 100644 index 0000000000000..6143933ba3d6c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_product_category_reindex_all.php @@ -0,0 +1,16 @@ +get(IndexerRegistry::class); + +$model = $indexRegistry->get(ProductCategoryIndexer::INDEXER_ID); +$model->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_catalog_product_reindex_schedule_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_catalog_product_reindex_schedule_rollback.php index 429f89abb6ae7..8909b258b9f9c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_catalog_product_reindex_schedule_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/enable_catalog_product_reindex_schedule_rollback.php @@ -5,8 +5,14 @@ */ declare(strict_types=1); -use Magento\Catalog\Model\Indexer\Product\Price\Processor; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\TestFramework\Helper\Bootstrap; -$indexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); -$indexerProcessor->getIndexer()->setScheduled(false); +/** @var IndexerRegistry $indexRegistry */ +$indexRegistry = Bootstrap::getObjectManager()->get(IndexerRegistry::class); + +$model = $indexRegistry->get('catalog_category_product'); +$model->setScheduled(false); + +$model = $indexRegistry->get('catalog_product_category'); +$model->setScheduled(false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes.php new file mode 100644 index 0000000000000..8764e12916d8b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes.php @@ -0,0 +1,165 @@ +get(Config::class); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); + +for ($i = 1; $i <= 2; $i++) { + $attributeModel = $attributeFactory->create(); + $attributeModel->setData( + [ + 'attribute_code' => 'test_attribute_' . $i, + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Attribute ' . $i], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'], + 'position' => 3 - $i + ] + ); + $attribute = $attributeRepository->save($attributeModel); + $installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); +} + +CacheCleaner::cleanAll(); +$eavConfig->clear(); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setName('Simple Product1') + ->setSku('simple1') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(SourceType::TYPE_IN_CART) + ->setPrice(10) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('5.99'); +$simple1 = $productRepository->save($product); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setName('Simple Product2') + ->setSku('simple2') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(SourceType::TYPE_ON_GESTURE) + ->setPrice(20) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('15.99'); +$simple2 = $productRepository->save($product); + +/** @var CategoryInterfaceFactory $categoryInterfaceFactory */ +$categoryInterfaceFactory = $objectManager->get(CategoryInterfaceFactory::class); + +$category = $categoryInterfaceFactory->create(); +$category->isObjectNew(true); +$category->setId(3334) + ->setCreatedAt('2014-06-23 09:50:07') + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2/333') + ->setLevel(2) + ->setAvailableSortBy(['position', 'name']) + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setPostedProducts( + [ + $simple1->getId() => 10, + $simple2->getId() => 11 + ] + ); +$category->save(); + +/** @var Collection $indexerCollection */ +$indexerCollection = $objectManager->get(Collection::class); +$indexerCollection->load(); +/** @var Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes_rollback.php new file mode 100644 index 0000000000000..49e2b549552e6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attributes_rollback.php @@ -0,0 +1,63 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +foreach (['simple1', 'simple2'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (NoSuchEntityException $exception) { + //Product already removed + } +} + +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var GetCategoryByName $getCategoryByName */ +$getCategoryByName = $objectManager->get(GetCategoryByName::class); +$category = $getCategoryByName->execute('Category 1'); +try { + if ($category->getId()) { + $categoryRepository->delete($category); + } +} catch (NoSuchEntityException $exception) { + //Category already removed +} + +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + +try { + for ($i = 1; $i <= 2; $i++) { + $attribute = $attributeRepository->get('test_attribute_' . $i); + $attributeRepository->delete($attribute); + } +} catch (NoSuchEntityException $exception) { + //Attribute already removed +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php index 35d4cceb50845..683416bc0a7ac 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php @@ -202,7 +202,7 @@ private function updateUploader(): void $rootDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::ROOT); $destDir = $rootDirectory->getRelativePath( $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] - . DS . $this->mediaConfig->getBaseMediaPath() + . DIRECTORY_SEPARATOR . $this->mediaConfig->getBaseMediaPath() ); $tmpDir = $rootDirectory->getRelativePath( $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] @@ -226,7 +226,7 @@ private function moveImages(string $fileName): void $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] ); $fixtureDir = realpath(__DIR__ . '/../../_files'); - $tmpFilePath = $rootDirectory->getAbsolutePath($tmpDir . DS . $fileName); + $tmpFilePath = $rootDirectory->getAbsolutePath($tmpDir . DIRECTORY_SEPARATOR . $fileName); $this->fileDriver->createDirectory($tmpDir); $rootDirectory->getDriver()->copy( $fixtureDir . DIRECTORY_SEPARATOR . $fileName, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 01a6bfe7b39b6..3ca6754c77767 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -3330,4 +3330,89 @@ public function testUpdateImageByNameNotPrefixedWithSlash() $imageItems = $product->getMediaGalleryImages()->getItems(); $this->assertCount(0, $imageItems); } + + /** + * Tests that images are imported correctly + * + * @magentoDataFixture mediaImportImageFixture + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @dataProvider importImagesDataProvider + * @magentoAppIsolation enabled + * @param string $importFile + * @param string $productSku + * @param string $storeCode + * @param array $expectedImages + * @param array $select + */ + public function testImportImages( + string $importFile, + string $productSku, + string $storeCode, + array $expectedImages, + array $select = ['file', 'label', 'position'] + ): void { + $this->importDataForMediaTest($importFile); + $product = $this->getProductBySku($productSku, $storeCode); + $actualImages = array_map( + function (\Magento\Framework\DataObject $item) use ($select) { + return $item->toArray($select); + }, + $product->getMediaGalleryImages()->getItems() + ); + $this->assertEquals($expectedImages, array_values($actualImages)); + } + + /** + * @return array[] + */ + public function importImagesDataProvider(): array + { + return [ + [ + 'import_media_additional_images_storeview.csv', + 'simple', + 'default', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1 + ], + [ + 'file' => '/m/a/magento_additional_image_one.jpg', + 'label' => null, + 'position' => 2 + ], + [ + 'file' => '/m/a/magento_additional_image_two.jpg', + 'label' => null, + 'position' => 3 + ], + ] + ], + [ + 'import_media_additional_images_storeview.csv', + 'simple', + 'fixturestore', + [ + [ + 'file' => '/m/a/magento_image.jpg', + 'label' => 'Image Alt Text', + 'position' => 1 + ], + [ + 'file' => '/m/a/magento_additional_image_one.jpg', + 'label' => 'Additional Image Label One', + 'position' => 2 + ], + [ + 'file' => '/m/a/magento_additional_image_two.jpg', + 'label' => 'Additional Image Label Two', + 'position' => 3 + ], + ] + ] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images_storeview.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images_storeview.csv new file mode 100644 index 0000000000000..ed8755a73fcb1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_images_storeview.csv @@ -0,0 +1,2 @@ +"sku","store_view_code","additional_images","additional_image_labels" +"simple","fixturestore","magento_additional_image_one.jpg, magento_additional_image_two.jpg","Additional Image Label One,Additional Image Label Two" diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php index 0432649455abe..e3e3d3e3972e5 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteTest.php @@ -85,6 +85,7 @@ public function productDataProvider(): array 'sku' => 'test-product', 'name' => 'test product', 'price' => 150, + 'website_ids' => [1] ], 'expected_data' => [ [ @@ -104,6 +105,7 @@ public function productDataProvider(): array 'name' => 'test product', 'price' => 150, 'url_key' => 'test-product-url-key', + 'website_ids' => [1] ], 'expected_data' => [ [ @@ -123,6 +125,7 @@ public function productDataProvider(): array 'name' => 'test product', 'price' => 150, 'url_key' => 'test-product-url-key', + 'website_ids' => [1] ], 'expected_data' => [], ], @@ -201,6 +204,7 @@ public function existingUrlKeyProvider(): array 'name' => 'test-simple-product', 'price' => 150, 'url_key' => 'simple-product', + 'store_ids' => [1] ], 'with_autogenerated_existing_product_url_key' => [ 'type_id' => Type::TYPE_SIMPLE, @@ -208,6 +212,7 @@ public function existingUrlKeyProvider(): array 'sku' => 'test-simple-product', 'name' => 'simple product', 'price' => 150, + 'store_ids' => [1] ], 'with_specified_existing_category_url_key' => [ 'type_id' => Type::TYPE_SIMPLE, @@ -216,6 +221,7 @@ public function existingUrlKeyProvider(): array 'name' => 'test-simple-product', 'price' => 150, 'url_key' => 'category-1', + 'store_ids' => [1] ], 'with_autogenerated_existing_category_url_key' => [ 'type_id' => Type::TYPE_SIMPLE, @@ -223,6 +229,7 @@ public function existingUrlKeyProvider(): array 'sku' => 'test-simple-product', 'name' => 'category 1', 'price' => 150, + 'store_ids' => [1] ], ], ]; diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php index c3efd660792c0..82631220730de 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Observer/ProductProcessUrlRewriteSavingObserverTest.php @@ -5,25 +5,46 @@ */ namespace Magento\CatalogUrlRewrite\Observer; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\UrlRewrite\Model\UrlFinderInterface; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; +use PHPUnit\Framework\TestCase; /** * @magentoAppArea adminhtml * @magentoDbIsolation disabled */ -class ProductProcessUrlRewriteSavingObserverTest extends \PHPUnit\Framework\TestCase +class ProductProcessUrlRewriteSavingObserverTest extends TestCase { - /** @var \Magento\Framework\ObjectManagerInterface */ - protected $objectManager; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; /** * Set up */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); } /** @@ -32,8 +53,8 @@ protected function setUp(): void */ private function getActualResults(array $filter) { - /** @var \Magento\UrlRewrite\Model\UrlFinderInterface $urlFinder */ - $urlFinder = $this->objectManager->get(\Magento\UrlRewrite\Model\UrlFinderInterface::class); + /** @var UrlFinderInterface $urlFinder */ + $urlFinder = $this->objectManager->get(UrlFinderInterface::class); $actualResults = []; foreach ($urlFinder->findAllByData($filter) as $url) { $actualResults[] = [ @@ -53,16 +74,14 @@ private function getActualResults(array $filter) */ public function testUrlKeyHasChangedInGlobalContext() { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository*/ - $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $product*/ - $product = $productRepository->get('product1'); + $testStore1 = $this->storeManager->getStore('default'); + $testStore4 = $this->storeManager->getStore('test'); - /** @var StoreManagerInterface $storeManager */ - $storeManager = $this->objectManager->get(StoreManagerInterface::class); - $storeManager->setCurrentStore(0); + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); - $testStore = $storeManager->getStore('test'); $productFilter = [ UrlRewrite::ENTITY_TYPE => 'product', ]; @@ -73,14 +92,14 @@ public function testUrlKeyHasChangedInGlobalContext() 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore4->getId(), ], ]; $actual = $this->getActualResults($productFilter); @@ -91,7 +110,7 @@ public function testUrlKeyHasChangedInGlobalContext() $product->setData('save_rewrites_history', true); $product->setUrlKey('new-url'); $product->setUrlPath('new-path'); - $product->save(); + $this->productRepository->save($product); $expected = [ [ @@ -99,28 +118,28 @@ public function testUrlKeyHasChangedInGlobalContext() 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], [ 'request_path' => "new-url.html", 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore4->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "new-url.html", 'is_auto_generated' => 0, 'redirect_type' => 301, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "new-url.html", 'is_auto_generated' => 0, 'redirect_type' => 301, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore4->getId(), ], ]; @@ -136,16 +155,13 @@ public function testUrlKeyHasChangedInGlobalContext() */ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository*/ - $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $product*/ - $product = $productRepository->get('product1'); + $testStore1 = $this->storeManager->getStore('default'); + $testStore4 = $this->storeManager->getStore('test'); - /** @var StoreManagerInterface $storeManager */ - $storeManager = $this->objectManager->get(StoreManagerInterface::class); - $storeManager->setCurrentStore(1); + $this->storeManager->setCurrentStore($testStore1); - $testStore = $storeManager->getStore('test'); + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); $productFilter = [ UrlRewrite::ENTITY_TYPE => 'product', @@ -154,7 +170,7 @@ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() $product->setData('save_rewrites_history', true); $product->setUrlKey('new-url'); $product->setUrlPath('new-path'); - $product->save(); + $this->productRepository->save($product); $expected = [ [ @@ -162,21 +178,21 @@ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore4->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "new-url.html", 'is_auto_generated' => 0, 'redirect_type' => 301, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), ], ]; @@ -192,16 +208,13 @@ public function testUrlKeyHasChangedInStoreviewContextWithPermanentRedirection() */ public function testUrlKeyHasChangedInStoreviewContextWithoutPermanentRedirection() { - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository*/ - $productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $product*/ - $product = $productRepository->get('product1'); + $testStore1 = $this->storeManager->getStore('default'); + $testStore4 = $this->storeManager->getStore('test'); - /** @var StoreManagerInterface $storeManager */ - $storeManager = $this->objectManager->get(StoreManagerInterface::class); - $storeManager->setCurrentStore(1); + $this->storeManager->setCurrentStore(1); - $testStore = $storeManager->getStore('test'); + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); $productFilter = [ UrlRewrite::ENTITY_TYPE => 'product', @@ -210,7 +223,7 @@ public function testUrlKeyHasChangedInStoreviewContextWithoutPermanentRedirectio $product->setData('save_rewrites_history', false); $product->setUrlKey('new-url'); $product->setUrlPath('new-path'); - $product->save(); + $this->productRepository->save($product); $expected = [ [ @@ -218,17 +231,402 @@ public function testUrlKeyHasChangedInStoreviewContextWithoutPermanentRedirectio 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => 1, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testAddAndRemoveProductFromWebsite() + { + $testStore1 = $this->storeManager->getStore('default'); + $testStore2 = $this->storeManager->getStore('fixture_second_store'); + $testStore3 = $this->storeManager->getStore('fixture_third_store'); + $testStore4 = $this->storeManager->getStore('test'); + + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); + + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + //Product in 1st website. Should result in being in 1st and 4th stores. + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Add product to websites corresponding to all 4 stores. + //Rewrites should be present for all stores. + $product->setWebsiteIds( + array_unique( + [ + $testStore1->getWebsiteId(), + $testStore2->getWebsiteId(), + $testStore3->getWebsiteId(), + $testStore4->getWebsiteId(), + ] + ) + ); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore3->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ] + ]; + + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Remove product from stores 1 and 4 and leave assigned to stores 2 and 3. + $product->setWebsiteIds( + array_unique( + [ + $testStore2->getWebsiteId(), + $testStore3->getWebsiteId(), + ] + ) + ); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore3->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testChangeVisibilityGlobalScope() + { + $testStore1 = $this->storeManager->getStore('default'); + $testStore2 = $this->storeManager->getStore('fixture_second_store'); + $testStore3 = $this->storeManager->getStore('fixture_third_store'); + $testStore4 = $this->storeManager->getStore('test'); + + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); + + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + //Product in 1st website. Should result in being in 1st and 4th stores. + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ] + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Set product to be not visible at global scope + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); + $this->productRepository->save($product); + $this->assertEmpty($this->getActualResults($productFilter)); + + //Add product to websites corresponding to all 4 stores. + //Rewrites should not be present as the product is hidden + //at the global scope. + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product->setWebsiteIds( + array_unique( + [ + $testStore1->getWebsiteId(), + $testStore2->getWebsiteId(), + $testStore3->getWebsiteId(), + $testStore4->getWebsiteId(), + ] + ) + ); + $this->productRepository->save($product); + $expected = []; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Set product to be visible at global scope + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product->setVisibility(Visibility::VISIBILITY_BOTH); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore3->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_rewrite_multistore.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testChangeVisibilityLocalScope() + { + $testStore1 = $this->storeManager->getStore('default'); + $testStore2 = $this->storeManager->getStore('fixture_second_store'); + $testStore3 = $this->storeManager->getStore('fixture_third_store'); + $testStore4 = $this->storeManager->getStore('test'); + + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + + /** @var Product $product*/ + $product = $this->productRepository->get('product1'); + + $productFilter = [ + UrlRewrite::ENTITY_TYPE => 'product', + ]; + + //Product in 1st website. Should result in being in 1st and 4th stores. + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + + //Set product to be not visible at store 4 scope + //Rewrite should only be present for store 1 + $this->storeManager->setCurrentStore($testStore4); + $product = $this->productRepository->get('product1'); + $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + self::assertCount(count($expected), $actual); + + //Add product to websites corresponding to all 4 stores. + //Rewrites should be present for stores 1,2 and 3. + //No rewrites should be present for store 4 as that is not visible + //at local scope. + $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $product = $this->productRepository->get('product1'); + $product->getExtensionAttributes()->setWebsiteIds( + array_unique( + [ + $testStore1->getWebsiteId(), + $testStore2->getWebsiteId(), + $testStore3->getWebsiteId(), + $testStore4->getWebsiteId() + ], + ) + ); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), ], [ 'request_path' => "product-1.html", 'target_path' => "catalog/product/view/id/" . $product->getId(), 'is_auto_generated' => 1, 'redirect_type' => 0, - 'store_id' => $testStore->getId(), + 'store_id' => $testStore3->getId(), ], ]; + $actual = $this->getActualResults($productFilter); + foreach ($expected as $row) { + $this->assertContains($row, $actual); + } + //Set product to be visible at store 4 scope only + $this->storeManager->setCurrentStore($testStore4); + $product = $this->productRepository->get('product1'); + $product->setVisibility(Visibility::VISIBILITY_BOTH); + $this->productRepository->save($product); + $expected = [ + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore1->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore2->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore3->getId(), + ], + [ + 'request_path' => "product-1.html", + 'target_path' => "catalog/product/view/id/" . $product->getId(), + 'is_auto_generated' => 1, + 'redirect_type' => 0, + 'store_id' => $testStore4->getId(), + ], + ]; $actual = $this->getActualResults($productFilter); foreach ($expected as $row) { $this->assertContainsEquals($row, $actual); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewritesTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewritesTest.php new file mode 100644 index 0000000000000..f958027f413e3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Model/Product/UpdateProductWebsiteUrlRewritesTest.php @@ -0,0 +1,72 @@ +action = $objectManager->get(Action::class); + $this->storeWebsiteRelation = $objectManager->get(StoreWebsiteRelationInterface::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + */ + public function testUpdateUrlRewrites() + { + /** @var Website $website */ + $websiteRepository = Bootstrap::getObjectManager()->get(WebsiteRepository::class); + $website = $websiteRepository->get('test'); + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $product = $productRepository->get('simple1', false, null, true); + $this->action->updateWebsites([$product->getId()], [$website->getId()], 'add'); + $storeIds = $this->storeWebsiteRelation->getStoreByWebsiteId($website->getId()); + + $this->assertStringContainsString( + $product->getUrlKey() . '.html', + $product->setStoreId(reset($storeIds))->getProductUrl() + ); + + $this->action->updateWebsites([$product->getId()], [$website->getId()], 'remove'); + $product->setRequestPath(''); + $url = $product->setStoreId(reset($storeIds))->getProductUrl(); + $this->assertStringNotContainsString( + $product->getUrlKey() . '.html', + $url + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix.php new file mode 100644 index 0000000000000..30d1b7512a11a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix.php @@ -0,0 +1,19 @@ +get(ConfigInterface::class); +$config->saveConfig('catalog/seo/product_url_suffix', null); +$config->saveConfig('catalog/seo/category_url_suffix', null); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); + +Resolver::getInstance()->requireDataFixture('Magento/CatalogUrlRewrite/_files/product_with_category.php'); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix_rollback.php new file mode 100644 index 0000000000000..5cf753e04ca7b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_empty_url_suffix_rollback.php @@ -0,0 +1,19 @@ +get(ConfigInterface::class); +$config->deleteConfig('catalog/seo/product_url_suffix'); +$config->deleteConfig('catalog/seo/category_url_suffix'); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); + +Resolver::getInstance()->requireDataFixture('Magento/CatalogUrlRewrite/_files/product_with_category_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php index 4cb4b00d08a84..e54ce16051d60 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/GuestShippingInformationManagementTest.php @@ -12,6 +12,8 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -120,6 +122,51 @@ public function testDifferentAddresses(bool $swapShipping): void $this->management->saveAddressInformation($idMask->getMaskedId(), $shippingInformation); } + /** + * Test save address information with customer custom address attribute for quote + * + * @return void + * + * @throws LocalizedException + * @throws NoSuchEntityException + * @magentoDataFixture Magento/Sales/_files/quote.php + * @magentoDataFixture Magento/Customer/_files/customer_address_with_custom_text_attribute.php + */ + public function testSaveAddressInformationWithCustomerCustomAddressAttribute(): void + { + $carts = $this->cartRepo->getList( + $this->searchCriteria->addFilter('reserved_order_id', 'test01')->create() + )->getItems(); + $currentQuote = array_pop($carts); + $guestCustomer = $this->customerRepo->get('JohnDoe@mail.com'); + + $customerCustomAddressAttribute = $guestCustomer->getCustomAttributes(); + + /** @var ShippingAssignmentInterface $shippingAssignment */ + $shippingAssignment = $currentQuote->getExtensionAttributes()->getShippingAssignments()[0]; + $shippingAddress = $shippingAssignment->getShipping()->getAddress(); + $billingAddress = $currentQuote->getBillingAddress(); + + if ($customerCustomAddressAttribute) { + $shippingAddress->setCustomAttributes($customerCustomAddressAttribute); + $billingAddress->setCustomAttributes($customerCustomAddressAttribute); + } + + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $this->shippingFactory->create(); + $shippingInformation->setBillingAddress($billingAddress); + $shippingInformation->setShippingAddress($shippingAddress); + $shippingInformation->setShippingMethodCode('flatrate'); + $shippingInformation->setShippingCarrierCode('flatrate'); + /** @var QuoteIdMask $idMask */ + $idMask = $this->maskFactory->create(); + $idMask->load($currentQuote->getId(), 'quote_id'); + + $paymentDetails = $this->management->saveAddressInformation($idMask->getMaskedId(), $shippingInformation); + $this->assertNotEmpty($paymentDetails); + $this->assertEquals($currentQuote->getGrandTotal(), $paymentDetails->getTotals()->getSubtotal()); + } + /** * Different variations for addresses test. * diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php index f0ba56f7179aa..4c003c209e3c2 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php @@ -67,7 +67,7 @@ $iDate++; break; case ProductCustomOptionInterface::OPTION_GROUP_FILE: - $value = 'test.jpg'; + $value = null; break; default: $value = 'test'; diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php index e9fa6d5bf96b7..20d366d05ac4a 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php @@ -57,30 +57,69 @@ protected function setUp(): void */ public function testGetRelationsByChildren(): void { - // Find configurable products options - $productOptionSkus = ['simple_10', 'simple_20', 'simple_30', 'simple_40']; - $searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', $productOptionSkus, 'in') + $childSkusOfParentSkus = [ + 'configurable' => ['simple_10', 'simple_20'], + 'configurable_12345' => ['simple_30', 'simple_40'], + ]; + $configurableSkus = [ + 'configurable', + 'configurable_12345', + 'simple_10', + 'simple_20', + 'simple_30', + 'simple_40', + ]; + $configurableIdsOfSkus = []; + + $searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', $configurableSkus, 'in') ->create(); - $productOptions = $this->productRepository->getList($searchCriteria) + $configurableProducts = $this->productRepository->getList($searchCriteria) ->getItems(); - $productOptionsIds = []; + $childIds = []; - foreach ($productOptions as $productOption) { - $productOptionsIds[] = $productOption->getId(); + foreach ($configurableProducts as $product) { + $configurableIdsOfSkus[$product->getSku()] = $product->getId(); + + if ($product->getTypeId() != 'configurable') { + $childIds[] = $product->getId(); + } } - // Find configurable products - $searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', ['configurable', 'configurable_12345'], 'in') - ->create(); - $configurableProducts = $this->productRepository->getList($searchCriteria) - ->getItems(); + $parentIdsOfChildIds = []; + + foreach ($childSkusOfParentSkus as $parentSku => $childSkus) { + foreach ($childSkus as $childSku) { + $childId = $configurableIdsOfSkus[$childSku]; + $parentIdsOfChildIds[$childId][] = $configurableIdsOfSkus[$parentSku]; + } + } - // Assert there are configurable products ids in result of getRelationsByChildren method. - $result = $this->model->getRelationsByChildren($productOptionsIds); + /** + * Assert there are parent configurable products ids in result of getRelationsByChildren method + * and they are related to child ids. + */ + $result = $this->model->getRelationsByChildren($childIds); + $sortedResult = $this->sortParentIdsOfChildIds($result); + $sortedExpected = $this->sortParentIdsOfChildIds($parentIdsOfChildIds); - foreach ($configurableProducts as $configurableProduct) { - $this->assertContains($configurableProduct->getId(), $result); + $this->assertEquals($sortedExpected, $sortedResult); + } + + /** + * Sorts the "Parent Ids Of Child Ids" type of the array + * + * @param array $array + * @return array + */ + private function sortParentIdsOfChildIds(array $array): array + { + foreach ($array as $childId => &$parentIds) { + sort($parentIds, SORT_NUMERIC); } + + ksort($array, SORT_NUMERIC); + + return $array; } } diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php index 67a15c24ea410..253eaea3f0686 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php @@ -71,4 +71,62 @@ public function testCollecting(): void $this->assertTrue($objectSrcChecked); $this->assertTrue($mediaSrcChecked); } + + /** + * Test collecting configurations from multiple XML files for adminhtml area. + * + * @magentoAppArea adminhtml + * @return void + */ + public function testCollectingForAdminhtmlArea(): void + { + $policies = $this->collector->collect([]); + + $mediaSrcChecked = false; + $objectSrcChecked = false; + $this->assertNotEmpty($policies); + /** @var FetchPolicy $policy */ + foreach ($policies as $policy) { + $this->assertFalse($policy->isNoneAllowed()); + $this->assertFalse($policy->isSelfAllowed()); + $this->assertFalse($policy->isInlineAllowed()); + $this->assertFalse($policy->isEvalAllowed()); + $this->assertFalse($policy->isDynamicAllowed()); + $this->assertEmpty($policy->getSchemeSources()); + $this->assertEmpty($policy->getNonceValues()); + if ($policy->getId() === 'object-src') { + $this->assertInstanceOf(FetchPolicy::class, $policy); + $this->assertEquals( + [ + 'https://admin.magento.com', + 'https://devdocs.magento.com', + 'example.magento.com' + ], + $policy->getHostSources() + ); + $this->assertEquals( + [ + 'B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256', + 'B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF9=' => 'sha256' + ], + $policy->getHashes() + ); + $objectSrcChecked = true; + } elseif ($policy->getId() === 'media-src') { + $this->assertInstanceOf(FetchPolicy::class, $policy); + $this->assertEquals( + [ + 'https://admin.magento.com', + 'https://devdocs.magento.com', + 'example.magento.com' + ], + $policy->getHostSources() + ); + $this->assertEmpty($policy->getHashes()); + $mediaSrcChecked = true; + } + } + $this->assertTrue($objectSrcChecked); + $this->assertTrue($mediaSrcChecked); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AddressMetadataTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AddressMetadataTest.php index 647c386e2b784..4443d170e388a 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AddressMetadataTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AddressMetadataTest.php @@ -19,7 +19,6 @@ class AddressMetadataTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - CacheCleaner::cleanAll(); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $objectManager->configure( [ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php index c3e08b5294679..63d7019ee4f61 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/CustomerMetadataTest.php @@ -29,7 +29,6 @@ class CustomerMetadataTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - CacheCleaner::cleanAll(); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $objectManager->configure( [\Magento\Framework\Api\ExtensionAttribute\Config\Reader::class => [ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Group/ResolverTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Group/ResolverTest.php new file mode 100644 index 0000000000000..0f85a94f639d1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Group/ResolverTest.php @@ -0,0 +1,26 @@ +create(Resolver::class); + $groupId = $resolver->resolve($customerId); + $this->assertEquals($groupId, $expectedGroupId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute.php new file mode 100644 index 0000000000000..ebe4ad76405ef --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute.php @@ -0,0 +1,96 @@ +create(Config::class) + ->getEntityType('customer'); + +/** @var $attributeSet Set */ +$attributeSet = Bootstrap::getObjectManager() + ->create(Set::class); + +$select = Bootstrap::getObjectManager()->create( + Attribute::class, + [ + 'data' => [ + 'frontend_input' => 'text', + 'frontend_label' => ['test_text_attribute'], + 'sort_order' => 1, + 'backend_type' => 'varchar', + 'is_user_defined' => 1, + 'is_system' => 0, + 'is_used_in_grid' => 1, + 'is_required' => '0', + 'is_visible' => 1, + 'used_in_forms' => [ + 'customer_address_edit', + 'adminhtml_customer_address' + ], + 'attribute_set_id' => $entityType->getDefaultAttributeSetId(), + 'attribute_group_id' => $attributeSet->getDefaultGroupId($entityType->getDefaultAttributeSetId()), + 'entity_type_id' => $entityType->getId(), + 'default_value' => '', + ], + ] +); +$select->setAttributeCode('test_text_attribute'); +$select->save(); + +$customer = $objectManager + ->create(Customer::class); +$customer->setWebsiteId(1) + ->setEntityId(1) + ->setEntityTypeId($entityType->getId()) + ->setAttributeSetId($entityType->getDefaultAttributeSetId()) + ->setEmail('JohnDoe@mail.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('John') + ->setLastname('Doe') + ->setGender(2) + ->setTestTextAttribute('123'); +$customer->isObjectNew(true); +// Create address +$address = $objectManager->create(Address::class); +// default_billing and default_shipping information would not be saved, it is needed only for simple check +$address->addData( + [ + 'firstname' => 'Charles', + 'lastname' => 'Alston', + 'street' => '3781 Neuport Lane', + 'city' => 'Panola', + 'country_id' => 'US', + 'region_id' => '51', + 'postcode' => '30058', + 'telephone' => '770-322-3514', + 'default_billing' => 1, + 'default_shipping' => 1, + ] +); +// Assign customer and address +$customer->addAddress($address); +$customer->save(); +// Mark last address as default billing and default shipping for current customer +$customer->setDefaultBilling($address->getId()); +$customer->setDefaultShipping($address->getId()); +$customer->save(); + +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customer'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customer', $customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute_rollback.php new file mode 100644 index 0000000000000..3b276b77fbed5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_address_with_custom_text_attribute_rollback.php @@ -0,0 +1,33 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var $attribute Attribute */ +$attribute = Bootstrap::getObjectManager()->create( + Attribute::class +); +$attribute->loadByCode('customer', 'test_text_attribute'); +$attribute->delete(); + +/** @var Customer $customer */ +$customer = Bootstrap::getObjectManager() + ->create(Customer::class); +$customer->setWebsiteId(1); +$customer->loadByEmail('JohnDoe@mail.com'); +$customer->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php index f9a1d2923e5be..552040489e253 100644 --- a/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Dhl/Model/CarrierTest.php @@ -17,19 +17,22 @@ use Magento\Quote\Model\Quote\Address\RateRequest; use Magento\Quote\Model\Quote\Address\RateResult\Error; use Magento\Shipping\Model\Shipment\Request; +use Magento\Shipping\Model\Simplexml\Element as ShippingElement; use Magento\Shipping\Model\Tracking\Result\Status; use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\HTTP\AsyncClientInterfaceMock; -use Magento\Shipping\Model\Simplexml\Element as ShippingElement; +use PHPUnit\Framework\TestCase; /** * Test for DHL integration. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CarrierTest extends \PHPUnit\Framework\TestCase +class CarrierTest extends TestCase { + private const PRODUCT_NAME_SPECIAL_CHARS = 'Φυστίκι Ψημένο με Αλάτι Συσκευασία'; + /** * @var Carrier */ @@ -254,10 +257,16 @@ private function assertTrackingResult($expectedTrackingData, $trackingResults): * @param string $origCountryId * @param string $expectedRegionCode * @param string $destCountryId + * @param bool|null $isProductNameContainsSpecialChars + * @return void * @dataProvider requestToShipmentDataProvider */ - public function testRequestToShip(string $origCountryId, string $expectedRegionCode, string $destCountryId): void - { + public function testRequestToShip( + string $origCountryId, + string $expectedRegionCode, + string $destCountryId, + bool $isProductNameContainsSpecialChars = false + ): void { $this->config->setValue( 'shipping/origin/country_id', $origCountryId, @@ -274,6 +283,8 @@ public function testRequestToShip(string $origCountryId, string $expectedRegionC ) ] ); + $productName = $isProductNameContainsSpecialChars ? self::PRODUCT_NAME_SPECIAL_CHARS : 'item_name'; + //phpcs:enable Magento2.Functions.DiscouragedFunction $request = new Request( [ @@ -291,7 +302,7 @@ public function testRequestToShip(string $origCountryId, string $expectedRegionC ], 'items' => [ 'item1' => [ - 'name' => 'item_name', + 'name' => $productName, ], ], ], @@ -329,10 +340,15 @@ public function testRequestToShip(string $origCountryId, string $expectedRegionC $requestElement->Request->ServiceHeader->MessageReference = 'MAGE_SHIP_28TO32_Char_CHECKED'; $requestElement->Request->ServiceHeader->MessageTime = 'currentTime'; $requestElement->ShipmentDetails->Date = 'currentTime'; - $this->assertXmlStringEqualsXmlString( - $this->getExpectedLabelRequestXml($origCountryId, $destCountryId, $expectedRegionCode), - $requestElement->asXML() + + $expectedLabelRequest = $this->getExpectedLabelRequestXml( + $origCountryId, + $destCountryId, + $expectedRegionCode, + $isProductNameContainsSpecialChars ); + + $this->assertXmlStringEqualsXmlString($expectedLabelRequest, $requestElement->asXML()); } /** @@ -351,7 +367,10 @@ public function requestToShipmentDataProvider(): array ], [ 'DE', 'EU', 'DE' - ] + ], + [ + 'GB', 'EU', 'US', true + ], ]; } @@ -361,12 +380,14 @@ public function requestToShipmentDataProvider(): array * @param string $origCountryId * @param string $destCountryId * @param string $regionCode + * @param bool $isProductNameContainsSpecialChars * @return string */ private function getExpectedLabelRequestXml( string $origCountryId, string $destCountryId, - string $regionCode + string $regionCode, + bool $isProductNameContainsSpecialChars ): string { $countryNames = [ 'US' => 'United States Of America', @@ -387,6 +408,10 @@ private function getExpectedLabelRequestXml( $expectedRequestElement->Shipper->CountryName = $countryNames[$origCountryId]; $expectedRequestElement->RegionCode = $regionCode; + if ($isProductNameContainsSpecialChars) { + $expectedRequestElement->ShipmentDetails->Pieces->Piece->PieceContents = self::PRODUCT_NAME_SPECIAL_CHARS; + } + return $expectedRequestElement->asXML(); } diff --git a/dev/tests/integration/testsuite/Magento/Directory/Block/DataTest.php b/dev/tests/integration/testsuite/Magento/Directory/Block/DataTest.php index ea2368a35c2f2..ccc1ea3f2f0b9 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Block/DataTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Block/DataTest.php @@ -22,7 +22,6 @@ protected function setUp(): void public function testGetCountryHtmlSelect() { - CacheCleaner::cleanAll(); $result = $this->block->getCountryHtmlSelect(); $resultTwo = $this->block->getCountryHtmlSelect(); $this->assertEquals($result, $resultTwo); @@ -30,7 +29,6 @@ public function testGetCountryHtmlSelect() public function testGetRegionHtmlSelect() { - CacheCleaner::cleanAll(); $result = $this->block->getRegionHtmlSelect(); $resultTwo = $this->block->getRegionHtmlSelect(); $this->assertEquals($result, $resultTwo); diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php index a2865d52517fa..85bf40b342fb5 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/ConfigTest.php @@ -33,7 +33,6 @@ protected function setUp(): void public function testGetEntityAttributeCodes() { $entityType = 'test'; - CacheCleaner::cleanAll(); $entityAttributeCodes1 = $this->config->getEntityAttributeCodes($entityType); $this->assertEquals( [ @@ -60,7 +59,6 @@ public function testGetEntityAttributeCodesWithObject() $testEntityType = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Entity\Type::class) ->loadByCode('test'); $attributeSetId = $testEntityType->getDefaultAttributeSetId(); - CacheCleaner::cleanAll(); $object = new DataObject( [ 'attribute_set_id' => $attributeSetId, @@ -86,7 +84,6 @@ public function testGetEntityAttributeCodesWithObject() public function testGetAttributes() { $entityType = 'test'; - CacheCleaner::cleanAll(); $attributes1 = $this->config->getAttributes($entityType); $expectedAttributeCodes = [ 'attribute_for_search_1', @@ -111,7 +108,6 @@ public function testGetAttributes() public function testGetAttribute() { $entityType = 'test'; - CacheCleaner::cleanAll(); $attribute1 = $this->config->getAttribute($entityType, 'attribute_for_search_1'); $this->assertEquals('attribute_for_search_1', $attribute1->getAttributeCode()); $this->assertEquals('varchar', $attribute1->getBackendType()); @@ -153,8 +149,7 @@ public function testGetAttributeWithCacheUserDefinedAttribute() $config = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class); $updatedAttribute = $config->getAttribute($entityType, 'foo'); $this->assertEquals('foo', $updatedAttribute->getFrontendLabel()); - // Clean cache - CacheCleaner::cleanAll(); + CacheCleaner::clean(['eav']); $config = Bootstrap::getObjectManager()->create(\Magento\Eav\Model\Config::class); // Check that attribute data has changed $updatedAttributeAfterCacheClean = $config->getAttribute($entityType, 'foo'); diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php index b27e81129e1c3..cb7d288deb75a 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/Attribute/Frontend/DefaultFrontendTest.php @@ -55,7 +55,6 @@ class DefaultFrontendTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - CacheCleaner::cleanAll(); $this->objectManager = Bootstrap::getObjectManager(); $this->defaultFrontend = $this->objectManager->get(DefaultFrontend::class); diff --git a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/AttributeLoaderTest.php b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/AttributeLoaderTest.php index 7a0c66e7e2db2..817c37bca528b 100644 --- a/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/AttributeLoaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Eav/Model/Entity/AttributeLoaderTest.php @@ -32,7 +32,6 @@ class AttributeLoaderTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - CacheCleaner::cleanAll(); $this->objectManager = Bootstrap::getObjectManager(); $this->attributeLoader = $this->objectManager->get(AttributeLoader::class); $entityType = $this->objectManager->create(\Magento\Eav\Model\Entity\Type::class) diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template_new_user_notification.php b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template_new_user_notification.php new file mode 100644 index 0000000000000..d742eb1a01414 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Email/Model/_files/email_template_new_user_notification.php @@ -0,0 +1,21 @@ +create(\Magento\Email\Model\Template::class); +$template->setOptions(['area' => 'test area', 'store' => 1]); +$templateText = '{{trans "New User Notification Custom Text %first_name, ' . + '%last_name" first_name=$user.firstname last_name=$user.lastname}}'; +$template->setData( + [ + 'template_text' => $templateText, + 'template_code' => 'New User Notification Custom Code', + 'template_type' => \Magento\Email\Model\Template::TYPE_TEXT, + 'orig_template_code' => 'admin_emails_new_user_notification_template' + ] +); +$template->save(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Config/InitialTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Config/InitialTest.php index 43203d38e308f..2b3ebc217ab7b 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Config/InitialTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Config/InitialTest.php @@ -24,7 +24,6 @@ protected function setUp(): void public function testGetMetadata() { - CacheCleaner::cleanAll(); $this->assertEquals( $this->objectManager->create(Config::class)->getMetadata(), $this->objectManager->create(Config::class)->getMetadata() @@ -37,7 +36,6 @@ public function testGetMetadata() */ public function testGetData($scope) { - CacheCleaner::cleanAll(); $this->assertEquals( $this->objectManager->create(Config::class)->getData($scope), $this->objectManager->create(Config::class)->getData($scope) diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigLoaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigLoaderTest.php index 1a2b8772c2dc5..ed4e15bd78a0c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigLoaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/ObjectManager/ConfigLoaderTest.php @@ -24,7 +24,6 @@ protected function setUp(): void public function testLoad() { - CacheCleaner::cleanAll(); $data = $this->object->load('global'); $this->assertNotEmpty($data); $cachedData = $this->object->load('global'); diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Route/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Route/ConfigTest.php index ff37b7d847921..d0c8c6083caee 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Route/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Route/ConfigTest.php @@ -28,7 +28,6 @@ protected function setUp(): void */ public function testGetRouteFrontName($route, $scope) { - CacheCleaner::cleanAll(); $this->assertEquals( $this->objectManager->create(Config::class)->getRouteFrontName($route, $scope), $this->objectManager->create(Config::class)->getRouteFrontName($route, $scope) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php index 857c068efe188..b02ccdb21e687 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Communication/ConfigTest.php @@ -17,7 +17,9 @@ class ConfigTest extends \PHPUnit\Framework\TestCase */ public function testGetTopics() { - $topics = $this->getConfigInstance(__DIR__ . '/_files/valid_communication.xml')->getTopics(); + $topics = $this->getConfigInstance( + [__DIR__ . '/_files/valid_communication.xml', __DIR__ . '/_files/valid_communication_extra.xml'] + )->getTopics(); $expectedParsedTopics = include __DIR__ . '/_files/valid_communication_expected.php'; $this->assertEquals($expectedParsedTopics, $topics); } @@ -29,9 +31,11 @@ public function testGetTopics() public function testGetTopicsNumeric() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Service method specified in the definition of topic "customerDeletedNumbers" is not av'); + $this->expectExceptionMessage( + 'Service method specified in the definition of topic "customerDeletedNumbers" is not av' + ); - $this->getConfigInstance(__DIR__ . '/_files/valid_communication_numeric.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/valid_communication_numeric.xml'])->getTopics(); } // @codingStandardsIgnoreStart @@ -58,7 +62,7 @@ public function testGetTopicsNumericInvalid() $this->expectException(\Magento\Framework\Exception\LocalizedException::class); $this->expectExceptionMessage('The XML in file "0" is invalid:'); - $this->getConfigInstance(__DIR__ . '/_files/invalid_communication_numeric.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/invalid_communication_numeric.xml'])->getTopics(); } /** @@ -66,7 +70,9 @@ public function testGetTopicsNumericInvalid() */ public function testGetTopic() { - $topics = $this->getConfigInstance(__DIR__ . '/_files/valid_communication.xml')->getTopic('customerCreated'); + $topics = $this->getConfigInstance( + [__DIR__ . '/_files/valid_communication.xml', __DIR__ . '/_files/valid_communication_extra.xml'] + )->getTopic('customerCreated'); $expectedParsedTopics = include __DIR__ . '/_files/valid_communication_expected.php'; $this->assertEquals($expectedParsedTopics['customerCreated'], $topics); } @@ -80,7 +86,7 @@ public function testGetTopicInvalidName() $this->expectException(\Magento\Framework\Exception\LocalizedException::class); $this->expectExceptionMessage('Topic "invalidTopic" is not configured.'); - $this->getConfigInstance(__DIR__ . '/_files/valid_communication.xml')->getTopic('invalidTopic'); + $this->getConfigInstance([__DIR__ . '/_files/valid_communication.xml'])->getTopic('invalidTopic'); } /** @@ -88,9 +94,11 @@ public function testGetTopicInvalidName() public function testGetTopicsExceptionMissingRequest() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Either "request" or "schema" attribute must be specified for topic "customerUpdated"'); + $this->expectExceptionMessage( + 'Either "request" or "schema" attribute must be specified for topic "customerUpdated"' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_missing_request.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_missing_request.xml'])->getTopics(); } /** @@ -100,7 +108,7 @@ public function testGetTopicsExceptionNotExistingServiceMethod() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Service method specified in the definition of topic "customerRetrieved" is not'); - $this->getConfigInstance(__DIR__ . '/_files/communication_not_existing_service_method.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_not_existing_service_method.xml'])->getTopics(); } /** @@ -110,7 +118,7 @@ public function testGetTopicsExceptionNotExistingService() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Service method specified in the definition of topic "customerRetrieved" is not'); - $this->getConfigInstance(__DIR__ . '/_files/communication_not_existing_service.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_not_existing_service.xml'])->getTopics(); } /** @@ -118,9 +126,11 @@ public function testGetTopicsExceptionNotExistingService() public function testGetTopicsExceptionNoAttributes() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Either "request" or "schema" attribute must be specified for topic "customerRetrieved"'); + $this->expectExceptionMessage( + 'Either "request" or "schema" attribute must be specified for topic "customerRetrieved"' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_no_attributes.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_no_attributes.xml'])->getTopics(); } /** @@ -128,9 +138,11 @@ public function testGetTopicsExceptionNoAttributes() public function testGetTopicsExceptionInvalidResponseSchema() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Response schema definition for topic "customerUpdated" should reference existing'); + $this->expectExceptionMessage( + 'Response schema definition for topic "customerUpdated" should reference existing' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_response_not_existing_service.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_response_not_existing_service.xml'])->getTopics(); } /** @@ -138,9 +150,11 @@ public function testGetTopicsExceptionInvalidResponseSchema() public function testGetTopicsExceptionInvalidRequestSchema() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Request schema definition for topic "customerUpdated" should reference existing'); + $this->expectExceptionMessage( + 'Request schema definition for topic "customerUpdated" should reference existing' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_request_not_existing_service.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_request_not_existing_service.xml'])->getTopics(); } /** @@ -148,9 +162,12 @@ public function testGetTopicsExceptionInvalidRequestSchema() public function testGetTopicsExceptionMultipleHandlersSynchronousMode() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic "customerDeleted" is configured for synchronous requests, that is why it must'); + $this->expectExceptionMessage( + 'Topic "customerDeleted" is configured for synchronous requests, that is why it must' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_multiple_handlers_synchronous_mode.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_multiple_handlers_synchronous_mode.xml']) + ->getTopics(); } /** @@ -158,9 +175,11 @@ public function testGetTopicsExceptionMultipleHandlersSynchronousMode() public function testGetTopicsExceptionInvalidHandler() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Service method specified in the definition of handler "customHandler" for topic "custo'); + $this->expectExceptionMessage( + 'Service method specified in the definition of handler "customHandler" for topic "custo' + ); - $this->getConfigInstance(__DIR__ . '/_files/communication_not_existing_handler_method.xml')->getTopics(); + $this->getConfigInstance([__DIR__ . '/_files/communication_not_existing_handler_method.xml'])->getTopics(); } /** @@ -168,10 +187,12 @@ public function testGetTopicsExceptionInvalidHandler() public function testGetTopicsExceptionInvalidTopicNameInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name "customerAdded" and attribute "name" = "customerCreated" must be equal'); + $this->expectExceptionMessage( + 'Topic name "customerAdded" and attribute "name" = "customerCreated" must be equal' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_invalid_topic_name.php' )->getTopics(); } @@ -184,7 +205,7 @@ public function testGetTopicsExceptionTopicWithoutDataInEnv() $this->expectExceptionMessage('Topic "customerCreated" must contain data'); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_topic_without_data.php' )->getTopics(); } @@ -197,7 +218,7 @@ public function testGetTopicsExceptionTopicWithMissedKeysInEnv() $this->expectExceptionMessage('Topic "customerCreated" has missed keys: [response]'); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_topic_with_missed_keys.php' )->getTopics(); } @@ -210,7 +231,7 @@ public function testGetTopicsExceptionTopicWithExcessiveKeysInEnv() $this->expectExceptionMessage('Topic "customerCreated" has excessive keys: [some_incorrect_key]'); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_topic_with_excessive_keys.php' )->getTopics(); } @@ -220,10 +241,12 @@ public function testGetTopicsExceptionTopicWithExcessiveKeysInEnv() public function testGetTopicsExceptionTopicWithNonMatchedNameInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name "customerDeleted" and attribute "name" = "customerRemoved" must be equal'); + $this->expectExceptionMessage( + 'Topic name "customerDeleted" and attribute "name" = "customerRemoved" must be equal' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_with_non_matched_name.php' )->getTopics(); } @@ -233,10 +256,12 @@ public function testGetTopicsExceptionTopicWithNonMatchedNameInEnv() public function testGetTopicsExceptionMultipleHandlersSynchronousModeInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic "customerDeleted" is configured for synchronous requests, that is why it must'); + $this->expectExceptionMessage( + 'Topic "customerDeleted" is configured for synchronous requests, that is why it must' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_multiple_handlers_synchronous_mode.php' )->getTopics(); } @@ -246,10 +271,12 @@ public function testGetTopicsExceptionMultipleHandlersSynchronousModeInEnv() public function testGetTopicsExceptionInvalidRequestSchemaInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Request schema definition for topic "customerCreated" should reference existing service'); + $this->expectExceptionMessage( + 'Request schema definition for topic "customerCreated" should reference existing service' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_request_not_existing_service.php' )->getTopics(); } @@ -259,10 +286,12 @@ public function testGetTopicsExceptionInvalidRequestSchemaInEnv() public function testGetTopicsExceptionInvalidResponseSchemaInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Response schema definition for topic "customerCreated" should reference existing type o'); + $this->expectExceptionMessage( + 'Response schema definition for topic "customerCreated" should reference existing type o' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_response_not_existing_service.php' )->getTopics(); } @@ -272,10 +301,12 @@ public function testGetTopicsExceptionInvalidResponseSchemaInEnv() public function testGetTopicsExceptionInvalidMethodInHandlerInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Service method specified in the definition of handler "customerCreatedFirst" for topic'); + $this->expectExceptionMessage( + 'Service method specified in the definition of handler "customerCreatedFirst" for topic' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_not_existing_handler_method.php' )->getTopics(); } @@ -285,10 +316,12 @@ public function testGetTopicsExceptionInvalidMethodInHandlerInEnv() public function testGetTopicsExceptionWithDisabledHandlerInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Disabled handler "default" for topic "customerCreated" cannot be added to the config fi'); + $this->expectExceptionMessage( + 'Disabled handler "default" for topic "customerCreated" cannot be added to the config fi' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_with_disabled_handler.php' )->getTopics(); } @@ -298,10 +331,12 @@ public function testGetTopicsExceptionWithDisabledHandlerInEnv() public function testGetTopicsExceptionIncorrectRequestSchemaTypeInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Request schema type for topic "customerCreated" must be "object_interface" or "service_'); + $this->expectExceptionMessage( + 'Request schema type for topic "customerCreated" must be "object_interface" or "service_' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_incorrect_request_schema_type.php' )->getTopics(); } @@ -311,10 +346,12 @@ public function testGetTopicsExceptionIncorrectRequestSchemaTypeInEnv() public function testGetTopicsExceptionIsNotBooleanTypeOfIsSynchronousInEnv() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The attribute "is_synchronous" for topic "customerCreated" should have the value of the'); + $this->expectExceptionMessage( + 'The attribute "is_synchronous" for topic "customerCreated" should have the value of the' + ); $this->getConfigInstance( - __DIR__ . '/_files/valid_communication.xml', + [__DIR__ . '/_files/valid_communication.xml'], __DIR__ . '/_files/communication_is_synchronous_is_not_boolean.php' )->getTopics(); } @@ -322,16 +359,20 @@ public function testGetTopicsExceptionIsNotBooleanTypeOfIsSynchronousInEnv() /** * Create config instance initialized with configuration from $configFilePath * - * @param string $configFilePath + * @param array $configFilePaths * @param string|null $envConfigFilePath * @return \Magento\Framework\Communication\ConfigInterface */ - protected function getConfigInstance($configFilePath, $envConfigFilePath = null) + protected function getConfigInstance($configFilePaths, $envConfigFilePath = null) { $fileResolver = $this->getMockForAbstractClass(\Magento\Framework\Config\FileResolverInterface::class); + $fileResolverResult = []; + foreach ($configFilePaths as $configFilePath) { + $fileResolverResult[] = file_get_contents($configFilePath); + } $fileResolver->expects($this->any()) ->method('get') - ->willReturn([file_get_contents($configFilePath)]); + ->willReturn($fileResolverResult); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $xmlReader = $objectManager->create( \Magento\Framework\Communication\Config\Reader\XmlReader::class, diff --git a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_expected.php b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_expected.php index e384e779de74d..447197e10808e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_expected.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_expected.php @@ -33,6 +33,10 @@ 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, 'method' => 'delete', ], + 'customerCreatedExtra' => [ + 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, + 'method' => 'save', + ], 'saveNameNotDisabled' => [ 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, 'method' => 'save', diff --git a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_extra.xml b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_extra.xml new file mode 100644 index 0000000000000..fad468a5d288a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_extra.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_input.php b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_input.php index e099d5d45c877..082984ff15aaf 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_input.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Communication/_files/valid_communication_input.php @@ -35,6 +35,10 @@ 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, 'method' => 'delete', ], + 'customerCreatedExtra' => [ + 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, + 'method' => 'save', + ], 'saveNameNotDisabled' => [ 'type' => \Magento\Customer\Api\CustomerRepositoryInterface::class, 'method' => 'save', diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php index 6e3391bd8959f..a93e8c198336e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php @@ -27,7 +27,6 @@ protected function setUp(): void set_error_handler(null); $this->resourceConnection = Bootstrap::getObjectManager() ->get(ResourceConnection::class); - CacheCleaner::cleanAll(); } protected function tearDown(): void diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php index ba2225fbe5eac..b77807a11da9b 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php @@ -6,6 +6,7 @@ namespace Magento\Framework\Mview\View; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Mview\View; /** * Test Class for \Magento\Framework\Mview\View\Changelog @@ -123,6 +124,54 @@ public function testClear() $this->assertEquals(1, $this->model->getVersion()); //the same that a table is empty } + /** + * Create entity table for MView + * + * @param string $tableName + * @return void + */ + private function createEntityTable(string $tableName) + { + $table = $this->resource->getConnection()->newTable( + $tableName + )->addColumn( + 'entity_id', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true], + 'Version ID' + )->addColumn( + 'additional_column', + \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, + null, + ['unsigned' => true, 'nullable' => false, 'default' => '0'], + 'Entity ID' + ); + $this->resource->getConnection()->createTable($table); + } + + public function testAdditionalColumns() + { + $tableName = 'test_mview_table'; + $this->createEntityTable($tableName); + $view = $this->objectManager->create(View::class); + $view->load('test_view_with_additional_columns'); + $view->subscribe(); + $this->connection->insert($tableName, ['entity_id' => 12, 'additional_column' => 13]); + $select = $this->connection->select() + ->from($view->getChangelog()->getName(), ['entity_id', 'test_additional_column']); + $actual = $this->connection->fetchAll($select); + $this->assertEquals( + [ + 'entity_id' => "12", + 'test_additional_column' => "13" + ], + reset($actual) + ); + $this->connection->dropTable($tableName); + $this->connection->dropTable($view->getChangelog()->getName()); + } + /** * Test for getList() method * diff --git a/dev/tests/integration/testsuite/Magento/Framework/Reflection/MethodsMapTest.php b/dev/tests/integration/testsuite/Magento/Framework/Reflection/MethodsMapTest.php index 585933422edb8..cbe3c552544d4 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Reflection/MethodsMapTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Reflection/MethodsMapTest.php @@ -24,7 +24,6 @@ protected function setUp(): void public function testGetMethodsMap() { - CacheCleaner::cleanAll(); $data = $this->object->getMethodsMap(\Magento\Framework\Reflection\MethodsMap::class); $this->assertArrayHasKey('getMethodsMap', $data); $cachedData = $this->object->getMethodsMap(\Magento\Framework\Reflection\MethodsMap::class); @@ -33,7 +32,6 @@ public function testGetMethodsMap() public function testGetMethodParams() { - CacheCleaner::cleanAll(); $data = $this->object->getMethodParams( \Magento\Framework\Reflection\MethodsMap::class, 'getMethodParams' diff --git a/dev/tests/integration/testsuite/Magento/Framework/TranslateTest.php b/dev/tests/integration/testsuite/Magento/Framework/TranslateTest.php index 5b48a9169c577..b216831bde33e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/TranslateTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/TranslateTest.php @@ -94,7 +94,6 @@ protected function setUp(): void public function testLoadData() { $data = $this->translate->loadData(null, true)->getData(); - CacheCleaner::cleanAll(); $this->translate->loadData()->getData(); $dataCached = $this->translate->loadData()->getData(); $this->assertEquals($data, $dataCached); diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Element/UiComponent/Config/Provider/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Element/UiComponent/Config/Provider/TemplateTest.php index f58882ee51493..92a51b85c8c4b 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/Element/UiComponent/Config/Provider/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Element/UiComponent/Config/Provider/TemplateTest.php @@ -49,7 +49,6 @@ public function testGetTemplate() \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('adminhtml'); $this->objectManager->get(\Magento\Framework\View\DesignInterface::class) ->setDesignTheme('FrameworkViewUiComponent/default'); - CacheCleaner::cleanAll(); $resultOne = $this->model->getTemplate('test.xml'); $resultTwo = $this->model->getTemplate('test.xml'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_gift_options.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_gift_options.php new file mode 100644 index 0000000000000..c870fa53c5e39 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/set_gift_options.php @@ -0,0 +1,33 @@ +get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = $objectManager->get(QuoteResource::class); +/** @var CartRepositoryInterface $cartRepository */ +$cartRepository = $objectManager->get(CartRepositoryInterface::class); + + +/** @var \Magento\GiftMessage\Model\Message $message */ +$message = $objectManager->create(\Magento\GiftMessage\Model\Message::class); +$message->setSender('Romeo'); +$message->setRecipient('Mercutio'); +$message->setMessage('I thought all for the best.'); +$message->save(); + +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_quote', 'reserved_order_id'); +$quote->setGiftMessageId($message->getId()); +$cartRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php index 038a8c7255815..72a093dc48e79 100644 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/ProductsCacheTest.php @@ -139,4 +139,33 @@ public function testDifferentProductsRequestsUseDifferentPageCacheRecords(): voi $this->assertEquals('MISS', $responseProduct1->getHeader('X-Magento-Cache-Debug')->getFieldValue()); $this->assertEquals('MISS', $responseProduct2->getHeader('X-Magento-Cache-Debug')->getFieldValue()); } + + /** + * Test response has category tags when products are filtered by category id + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testProductsFilterByCategoryHasCategoryTags(): void + { + $query + = <<dispatchGraphQlGETRequest(['query' => $query]); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $expectedCacheTags = ['cat_c', 'cat_c_333']; + + foreach ($expectedCacheTags as $cacheTag) { + $this->assertContains($cacheTag, $actualCacheTags); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/invalid_catalog_products.csv b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/invalid_catalog_products.csv new file mode 100644 index 0000000000000..f873a7c89ee53 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Import/_files/invalid_catalog_products.csv @@ -0,0 +1,3 @@ +sku,_store,_attribute_set,product_type,categories,_product_websites,color,cost,country_of_manufacture,created_at,custom_design,custom_design_from,custom_design_to,custom_layout_update,description,gallery,gift_message_available,gift_wrapping_available,gift_wrapping_price,has_options,image,image_label,is_returnable,manufacturer,meta_description,meta_keyword,meta_title,minimal_price,msrp,msrp_display_actual_price_type,name,news_from_date,news_to_date,options_container,page_layout,price,quantity_and_stock_status,related_tgtr_position_behavior,related_tgtr_position_limit,required_options,short_description,small_image,small_image_label,special_from_date,special_price,special_to_date,status,tax_class_id,thumbnail,thumbnail_label,updated_at,upsell_tgtr_position_behavior,upsell_tgtr_position_limit,url_key,url_path,visibility,weight,qty,min_qty,use_config_min_qty,is_qty_decimal,backorders,use_config_backorders,min_sale_qty,use_config_min_sale_qty,max_sale_qty,use_config_max_sale_qty,is_in_stock,notify_stock_qty,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,_related_sku,_related_position,_crosssell_sku,_crosssell_position,_upsell_sku,_upsell_position,_tier_price_website,_tier_price_customer_group,_tier_price_qty,_tier_price_price,_media_attribute_id,_media_image,_media_label,_media_position,_media_is_disabled,_associated_sku,_associated_default_qty,_associated_position +"",,Default,simple,,base,,,,"2014-12-25 19:52:47",,,,,,,,,,0,,,"No",,"simple product 1 ","simple product 1","simple product 1",,,,"simple product 1",,,"Block after Info Column",,100.0000,"In Stock",,,0,,,,,,,1,2,,,"2014-12-25 19:52:47",,,simple-product-1,,"catalog, search",,123.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,1,0,1,0.0000,1,0,0,1,,,,,,,,,,,,,,,,,, +"",,Default,simple,,base,,,,"2014-12-25 19:53:14",,,,,,,,,,0,,,"No",,"simple product 2 ","simple product 2","simple product 2",,,,"simple product 2",,,"Block after Info Column",,200.0000,"In Stock",,,0,,,,,,,1,2,,,"2014-12-25 19:53:14",,,simple-product-2,,"catalog, search",,234.0000,0.0000,1,0,0,1,1.0000,1,0.0000,1,1,,1,1,0,1,0.0000,1,0,0,1,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/ImportResultTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/ImportResultTest.php new file mode 100644 index 0000000000000..37b5bcd81f68d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/ImportResultTest.php @@ -0,0 +1,91 @@ +getRequest()->setParam('isAjax', true); + $this->getRequest()->setMethod('POST'); + $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'; + + /** @var $formKey \Magento\Framework\Data\Form\FormKey */ + $formKey = $this->_objectManager->get(\Magento\Framework\Data\Form\FormKey::class); + $this->getRequest()->setPostValue('form_key', $formKey->getFormKey()); + $this->getRequest()->setPostValue('entity', 'catalog_product'); + $this->getRequest()->setPostValue('behavior', 'append'); + $this->getRequest()->setPostValue(Import::FIELD_NAME_VALIDATION_STRATEGY, $validationStrategy); + $this->getRequest()->setPostValue(Import::FIELD_NAME_ALLOWED_ERROR_COUNT, 0); + $this->getRequest()->setPostValue('_import_field_separator', $delimiter); + + /** @var \Magento\TestFramework\App\Filesystem $filesystem */ + $filesystem = $this->_objectManager->get(\Magento\Framework\Filesystem::class); + $tmpDir = $filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); + $subDir = str_replace('\\', '_', __CLASS__); + $tmpDir->create($subDir); + $target = $tmpDir->getAbsolutePath("{$subDir}" . DIRECTORY_SEPARATOR . "{$fileName}"); + copy(__DIR__ . DIRECTORY_SEPARATOR . 'Import' . DIRECTORY_SEPARATOR . '_files' + . DIRECTORY_SEPARATOR . "{$fileName}", $target); + + $_FILES = [ + 'import_file' => [ + 'name' => $fileName, + 'type' => $mimeType, + 'tmp_name' => $target, + 'error' => 0, + 'size' => filesize($target) + ] + ]; + + $this->_objectManager->configure( + [ + 'preferences' => [FileTransferFactory::class => HttpFactoryMock::class] + ] + ); + + $this->dispatch('backend/admin/import/validate'); + $this->assertStringNotContainsString('<br>', $this->getResponse()->getBody()); + } + + /** + * @return array + */ + public function validationDataProvider(): array + { + return [ + [ + 'file_name' => 'invalid_catalog_products.csv', + 'mime-type' => 'text/csv', + 'delimiter' => ',', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin.php b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin.php new file mode 100755 index 0000000000000..66acb3c70f9be --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin.php @@ -0,0 +1,53 @@ +get(RoleFactory::class)->create(); +$role->setName('test_custom_role'); +$role->setData('role_name', $role->getName()); +$role->setRoleType(\Magento\Authorization\Model\Acl\Role\Group::ROLE_TYPE); +$role->setUserType((string)\Magento\Authorization\Model\UserContextInterface::USER_TYPE_ADMIN); + +/** @var RoleResource $roleResource */ +$roleResource = Bootstrap::getObjectManager()->get(RoleResource::class); +$roleResource->save($role); + +/** @var Rules $rules */ +$rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create(); +$rules->setRoleId($role->getId()); +//Granted all permissions. +$rules->setResources([Bootstrap::getObjectManager()->get(\Magento\Framework\Acl\RootResource::class)->getId()]); + +/** @var RulesResource $rulesResource */ +$rulesResource = Bootstrap::getObjectManager()->get(RulesResource::class); +$rulesResource->saveRel($rules); + +/** @var User $user */ +$user = Bootstrap::getObjectManager()->create(User::class); +$user->setFirstname("John") + ->setLastname("Doe") + ->setUsername('TestAdmin1') + ->setPassword(\Magento\TestFramework\Bootstrap::ADMIN_PASSWORD) + ->setEmail('testadmin1@gmail.com') + ->setIsActive(1) + ->setRoleId($role->getId()); + +/** @var UserResource $userResource */ +$userResource = Bootstrap::getObjectManager()->get(UserResource::class); +$userResource->save($user); \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin_rollback.php b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin_rollback.php new file mode 100755 index 0000000000000..aabfca018d974 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/admin_rollback.php @@ -0,0 +1,42 @@ +create(User::class); +$user->load('TestAdmin1', 'username'); + +/** @var UserResource $userResource */ +$userResource = Bootstrap::getObjectManager()->get(UserResource::class); +$userResource->delete($user); + +/** @var Role $role */ +$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); +$role->load('test_custom_role', 'role_name'); + +/** @var Rules $rules */ +$rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create(); +$rules->load($role->getId(), 'role_id'); + +/** @var RulesResource $rulesResource */ +$rulesResource = Bootstrap::getObjectManager()->get(RulesResource::class); +$rulesResource->delete($rules); + +/** @var RoleResource $roleResource */ +$roleResource = Bootstrap::getObjectManager()->get(RoleResource::class); +$roleResource->delete($role); diff --git a/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/customer.php b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/customer.php new file mode 100644 index 0000000000000..dba02bc340738 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/LoginAsCustomer/_files/customer.php @@ -0,0 +1,48 @@ +create(\Magento\Customer\Api\CustomerRepositoryInterface::class); +$customer = $objectManager->create(\Magento\Customer\Model\Customer::class); + +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +/** @var Magento\Customer\Model\Customer $customer */ +$customer->setWebsiteId(1) + ->setId(1) + ->setEmail('customer@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setTaxvat('12') + ->setGender(0); + +$extension = $customer->getExtensionAttributes(); +if ($extension === null) { + $extension = $objectManager->get(CustomerExtensionFactory::class)->create(); +} + +$extension->setAssistanceAllowed(2); +$customer->setExtensionAttributes($extension); + +$customer->isObjectNew(true); +$customer->save(); + +$customerRegistry->remove($customer->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Block/Checkout/OverviewTest.php b/dev/tests/integration/testsuite/Magento/Multishipping/Block/Checkout/OverviewTest.php index 5009f210404d0..182505cba4a61 100644 --- a/dev/tests/integration/testsuite/Magento/Multishipping/Block/Checkout/OverviewTest.php +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Block/Checkout/OverviewTest.php @@ -6,28 +6,58 @@ namespace Magento\Multishipping\Block\Checkout; +use Magento\Catalog\Model\ProductRepository; +use Magento\Checkout\Block\Cart\Item\Renderer; +use Magento\Framework\App\Area; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\RendererList; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + /** - * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * Verify default items template */ -class OverviewTest extends \PHPUnit\Framework\TestCase +class OverviewTest extends TestCase { /** - * @var \Magento\Multishipping\Block\Checkout\Overview + * @var Overview + */ + private $block; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Quote + */ + private $quote; + + /** + * @var ProductRepository */ - protected $_block; + private $product; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var Item */ - protected $_objectManager; + private $item; + /** + * @inheritdoc + */ protected function setUp(): void { - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_block = $this->_objectManager->get(\Magento\Framework\View\LayoutInterface::class) + Bootstrap::getInstance()->loadArea(Area::AREA_FRONTEND); + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class) ->createBlock( - \Magento\Multishipping\Block\Checkout\Overview::class, + Overview::class, 'checkout_overview', [ 'data' => [ @@ -37,33 +67,49 @@ protected function setUp(): void ] ); - $this->_block->addChild('renderer.list', \Magento\Framework\View\Element\RendererList::class); - $this->_block->getChildBlock( + $this->block->addChild('renderer.list', RendererList::class); + $this->block->getChildBlock( 'renderer.list' )->addChild( 'default', - \Magento\Checkout\Block\Cart\Item\Renderer::class, + Renderer::class, ['template' => 'cart/item/default.phtml'] ); + $this->quote = $this->objectManager->create(Quote::class); + $this->product = $this->objectManager->create(ProductRepository::class); + $this->item = $this->objectManager->create(Item::class); } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ public function testGetRowItemHtml() { - /** @var $item \Magento\Quote\Model\Quote\Item */ - $item = $this->_objectManager->create(\Magento\Quote\Model\Quote\Item::class); - /** @var $product \Magento\Catalog\Model\Product */ - $product = $this->_objectManager->create(\Magento\Catalog\Model\Product::class); - $product->load(1); - $item->setProduct($product); - /** @var $quote \Magento\Quote\Model\Quote */ - $quote = $this->_objectManager->create(\Magento\Quote\Model\Quote::class); - $item->setQuote($quote); + $product = $this->product->get('simple'); + $item = $this->item->setProduct($product); + $item->setQuote($this->quote); // assure that default renderer was obtained $this->assertEquals( 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( + Xpath::getElementsCountForXpath( '//*[contains(@class,"product") and contains(@class,"name")]/a', - $this->_block->getRowItemHtml($item) + $this->block->getRowItemHtml($item) + ) + ); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php + */ + public function testLinkOptionalProductFileItemHtml() + { + $quote = $this->quote->load('customer_quote_product_custom_options', 'reserved_order_id'); + $item = current($quote->getAllItems()); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + '//dd/a[contains(text(), "test.jpg")]', + $this->block->getRowItemHtml($item) ) ); } diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php index 97f59e94d9cfe..719d78b07ca3c 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/Plugin/PluginTest.php @@ -205,4 +205,22 @@ public function testCustomerWithZeroStoreIdIsSubscribed() $this->assertEquals($customer->getId(), (int)$subscriber->getCustomerId()); $this->assertEquals($currentStore, (int)$subscriber->getStoreId()); } + + /** + * Test get list customer, which have more then 2 subscribes in newsletter_subscriber. + * + * @magentoAppArea frontend + * @magentoDataFixture Magento/Newsletter/_files/subscribers.php + */ + public function testCustomerWithTwoNewsLetterSubscriptions() + { + /** @var \Magento\Framework\Api\SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\SearchCriteriaBuilder::class); + $searchCriteria = $searchBuilder->addFilter('entity_id', 1)->create(); + $items = $this->customerRepository->getList($searchCriteria)->getItems(); + /** @var \Magento\Customer\Api\Data\CustomerInterface $customer */ + $customer = $items[0]; + $extensionAttributes = $customer->getExtensionAttributes(); + $this->assertTrue($extensionAttributes->getIsSubscribed()); + } } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php index 287564722ced6..b28788bc649a1 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/ObserverTest.php @@ -93,7 +93,6 @@ public function testProcessPortuguese() $secondStore = $storeRepository->get('fixture_second_store'); // check if Portuguese language is specified for the second store - CacheCleaner::cleanAll(); $storeResolver = $this->_objectManager->get(Resolver::class); $storeResolver->emulate($secondStore->getId()); $this->assertEquals('pt_BR', $storeResolver->getLocale()); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index 081cae5f98ee5..94cf6ef108474 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -737,4 +737,28 @@ private function getCustomerDataArray(): array CustomerInterface::WEBSITE_ID => 1, ]; } + + /** + * @magentoConfigFixture current_store sales/minimum_order/active 1 + * @magentoConfigFixture current_store sales/minimum_order/amount 5 + * @magentoConfigFixture current_store sales/minimum_order/tax_including 1 + * @magentoConfigFixture current_store sales/minimum_order/include_discount_amount 1 + * @magentoConfigFixture current_store tax/calculation/price_includes_tax 1 + * @magentoConfigFixture current_store tax/calculation/apply_after_discount 1 + * @magentoConfigFixture current_store tax/calculation/cross_border_trade_enabled 1 + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_with_coupon_5_off_no_condition.php + * @magentoDataFixture Magento/Tax/_files/tax_rule_region_1_al.php + * @magentoDataFixture Magento/Checkout/_files/quote_with_taxable_product_and_customer.php + */ + public function testValidateMinimumAmountWithPriceInclTaxAndDiscount() + { + /** @var $quote \Magento\Quote\Model\Quote */ + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_taxable_product'); + $quote->setCouponCode('CART_FIXED_DISCOUNT_5'); + $quote->collectTotals(); + $this->assertEquals(-5, $quote->getShippingAddress()->getBaseDiscountAmount()); + $this->assertEquals(9.3, $quote->getShippingAddress()->getBaseSubtotal()); + $this->assertEquals(5, $quote->getShippingAddress()->getBaseGrandTotal()); + $this->assertTrue($quote->validateMinimumAmount()); + } } diff --git a/dev/tests/integration/testsuite/Magento/ReleaseNotification/Controller/Adminhtml/Dashboard/IndexTest.php b/dev/tests/integration/testsuite/Magento/ReleaseNotification/Controller/Adminhtml/Dashboard/IndexTest.php index 9358d761b28b2..ffe2a4d6ca1c5 100644 --- a/dev/tests/integration/testsuite/Magento/ReleaseNotification/Controller/Adminhtml/Dashboard/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/ReleaseNotification/Controller/Adminhtml/Dashboard/IndexTest.php @@ -34,6 +34,7 @@ protected function setUp(): void protected function tearDown(): void { $this->objectManager->removeSharedInstance(ContentProviderInterface::class); + CacheCleaner::clean(['layout']); parent::tearDown(); } @@ -44,7 +45,6 @@ public function testExecute() { $content = include __DIR__ . '/../../../_files/validContent.php'; - CacheCleaner::cleanAll(); $this->contentProviderMock->expects($this->any()) ->method('getContent') ->willReturn($content); @@ -62,7 +62,6 @@ public function testExecute() public function testExecuteEmptyContent() { - CacheCleaner::cleanAll(); $this->contentProviderMock->expects($this->any()) ->method('getContent') ->willReturn('[]'); @@ -77,7 +76,6 @@ public function testExecuteEmptyContent() public function testExecuteFalseContent() { - CacheCleaner::cleanAll(); $this->contentProviderMock->expects($this->any()) ->method('getContent') ->willReturn(false); diff --git a/dev/tests/integration/testsuite/Magento/Review/Ui/DataProvider/Product/ReviewDataProviderTest.php b/dev/tests/integration/testsuite/Magento/Review/Ui/DataProvider/Product/ReviewDataProviderTest.php new file mode 100644 index 0000000000000..5cd594a37d0e0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Ui/DataProvider/Product/ReviewDataProviderTest.php @@ -0,0 +1,138 @@ + 'review_listing_data_source', + 'primaryFieldName' => 'review_id', + 'requestFieldName' => 'entity_id', + ]; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Sorting dataProvider test + * + * @magentoDataFixture Magento/Review/_files/different_reviews.php + * @dataProvider sortingDataProvider + * + * @param string $field + * @param string $direction + * @param array $expectedSortedTitles + * @return void + */ + public function testSorting(string $field, string $direction, array $expectedSortedTitles): void + { + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('current_product_id', 1); + + $dataProvider = $this->objectManager->create( + ReviewDataProvider::class, + array_merge($this->modelParams, ['request' => $request]) + ); + $dataProvider->addOrder($field, $direction); + $result = $dataProvider->getData(); + + $this->assertEquals($this->getItemsField($result, 'title'), $expectedSortedTitles); + } + + /** + * Return items field data + * + * @param array $arrItems + * @param string $field + * @return array + */ + private function getItemsField(array $arrItems, string $field): array + { + $data = []; + foreach ($arrItems['items'] as $review) { + $data[] = $review[$field]; + } + + return $data; + } + + /** + * DataProvider for testSorting + * + * @return array + */ + public function sortingDataProvider(): array + { + return [ + 'sort by title field ascending' => [ + 'title', + 'asc', + ['1 filter second review', '2 filter first review', 'Review Summary'], + ], + 'sort by title field descending' => [ + 'title', + 'desc', + ['Review Summary', '2 filter first review', '1 filter second review'], + ], + ]; + } + + /** + * Filter dataProvider test + * + * @magentoDataFixture Magento/Review/_files/different_reviews.php + * + * @return void + */ + public function testFilter(): void + { + $searchTitle = '2 filter first review'; + + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('current_product_id', 1); + + /** @var ReviewDataProvider $dataProvider */ + $dataProvider = $this->objectManager->create( + ReviewDataProvider::class, + array_merge($this->modelParams, ['request' => $request]) + ); + + /** @var Filter $filter */ + $filter = $this->objectManager->create(Filter::class); + $filter->setField('title') + ->setValue($searchTitle); + + $dataProvider->addFilter($filter); + $result = $dataProvider->getData(); + + $this->assertEquals($this->getItemsField($result, 'title'), [$searchTitle]); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php index 861559acd8c20..b7e3ffcf9cd9d 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php @@ -9,6 +9,7 @@ namespace Magento\Sales\Block\Adminhtml\Order\Create\Form; +use Magento\Backend\Block\Template\Context; use Magento\Backend\Model\Session\Quote as SessionQuote; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AttributeMetadataInterface; @@ -17,6 +18,7 @@ use Magento\Customer\Model\Data\Option; use Magento\Customer\Model\Metadata\Form; use Magento\Customer\Model\Metadata\FormFactory; +use Magento\Framework\App\RequestInterface as Request; use Magento\Framework\View\LayoutInterface; use Magento\Quote\Model\Quote; use Magento\Store\Model\StoreManagerInterface; @@ -107,7 +109,7 @@ public function testGetFormWithCustomer() ); } - self::assertRegExp( + self::assertMatchesRegularExpression( '/